From 57d2235791f12a56054ef8eef0a71a07520dbbef Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Thu, 15 Jul 2021 19:32:29 +0800 Subject: [PATCH 01/26] =?UTF-8?q?feat(legend):=20=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E4=BA=86legend=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/types.ts | 169 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/src/ui/legend/types.ts b/src/ui/legend/types.ts index 4b8849e79..6e8e2c2e5 100644 --- a/src/ui/legend/types.ts +++ b/src/ui/legend/types.ts @@ -1 +1,168 @@ -export type LegendOptions = {}; +import { ShapeCfg, ShapeAttrs } from '../../types'; +import { MarkerAttrs } from '../marker/types'; + +// 状态样式:默认状态、hover状态、禁用状态、选择状态 +export type StyleStatus = 'active' | 'disabled' | 'checked'; +type MixAttrs = ShapeAttrs & + { + [key in StyleStatus]?: ShapeAttrs; + }; + +// marker配置 +type MarkerCfg = string | MarkerAttrs['symbol']; + +// 色板 +type RailCfg = { + // 色板类型 + type: 'color' | 'size'; + // 是否分块 + chunked: boolean; + // 分块连续图例分割点 + items: number[]; + // 是否使用渐变色 + isGradient: boolean | 'auto'; +}; + +// 滑动手柄 +type HandlerCfg = { + show: boolean; + size: number; + spacing: number; + icon: { + marker: MarkerCfg; + style: ShapeAttrs; + }; + text: { + style: ShapeAttrs; + formatter: (value: number) => string; + }; +}; + +// 图例项 +type CategoryItem = { + name: string; + value: number | string; + id: number | string; +}; + +// 图例项图标 +type ItemMarkerCfg = { + symbol: MarkerCfg; + size: number; + style: MixAttrs; +}; + +// 图例项Name +type ItemNameCfg = { + // name与marker的距离 + spacing: number; + style: MixAttrs; + formatter: (name: string, item: CategoryItem, index: number) => string; +}; + +// 图例项值 +type ItemValueCfg = { + spacing: number; + align: 'left' | 'right'; + style: MixAttrs; + formatter: (value: number, item: CategoryItem, index: number) => number | string; +}; + +// 图例项配置 +export type CategoryItemsCfg = { + items: CategoryItem[]; + itemCfg: { + // 单个图例项高度 + height: number; + // 单个图例项宽度 + width: number; + // 图例项间的间隔 + spacing: number; + marker: ItemMarkerCfg | ((item: CategoryItem, index: number, items: CategoryItem[]) => ItemMarkerCfg); + name: ItemNameCfg | ((item: CategoryItem, index: number, items: CategoryItem[]) => ItemNameCfg); + value: ItemValueCfg | ((item: CategoryItem, index: number, items: CategoryItem[]) => ItemValueCfg); + // 图例项背景 + backgroundStyle: MixAttrs | ((item: CategoryItem, index: number, items: CategoryItem[]) => MixAttrs); + }; +}; + +// 分页器 +type pageNavigatorCfg = { + // 按钮 + button: { + // 按钮图标 + marker: MarkerCfg | ((type: 'prev' | 'next') => MarkerCfg); + // 按钮状态样式 + style: MixAttrs; + }; + // 页码 + pagination: { + style: ShapeAttrs; + divider: string; + formatter: (pageNumber: number) => number | string; + }; +}; + +export type LegendBaseCfg = ShapeCfg & { + // 宽度 + width: number; + // 高度 + height: number; + // 图例内边距 + padding: number | number[]; + // 背景 + background: MixAttrs; + // 布局 + orient: 'horizontal' | 'vertical'; + // Legend类型 + type: 'category' | 'continuous'; + // 指示器 + indicator: false | {}; +}; + +export type LegendBaseOptions = ShapeCfg & { + attrs: LegendBaseCfg; +}; + +// 连续图例配置 +export type ContinuousCfg = LegendBaseCfg & { + // 最小值 + min: number; + // 最大值 + max: number; + // 选择区域 + value: [number, number]; + // 色板颜色 + color: string | string[]; + // 标签 + label: { + style: ShapeAttrs; + spacing: number; + formatter: (value: number) => number | string; + align: 'rail' | 'top' | 'bottom'; + offset: [number, number]; + }; + // 色板配置 + rail: RailCfg; + // 是否可滑动 + slidable: boolean; + // 滑动步长 + step: number; + // 手柄配置 + handler: HandlerCfg; +}; + +export type ContinuousOptions = ShapeCfg & { + attrs: ContinuousCfg; +}; + +// 分类图例配置 +export type CategoryCfg = LegendBaseCfg & { + items: CategoryItemsCfg; + reverse: boolean; + pageNavigator: false | pageNavigatorCfg; +}; + +export type CategoryOptions = ShapeCfg & { + attrs: CategoryCfg; +}; From b3f979dfa94b3c0aa997a7f7e468bd9be60414d3 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Thu, 15 Jul 2021 19:32:47 +0800 Subject: [PATCH 02/26] =?UTF-8?q?feat(legend):=20=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=BA=86legend=E5=9F=BA=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/base.ts | 91 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/ui/legend/base.ts diff --git a/src/ui/legend/base.ts b/src/ui/legend/base.ts new file mode 100644 index 000000000..8ae3881d0 --- /dev/null +++ b/src/ui/legend/base.ts @@ -0,0 +1,91 @@ +import { deepMix, get, isNumber } from '@antv/util'; +import { Rect } from '@antv/g'; +import { GUI } from '../core/gui'; +import { LegendBaseCfg, LegendBaseOptions, StyleStatus } from './types'; + +export default abstract class LegendBase extends GUI { + public static tag = 'legendBase'; + + // background + private backgroundShape: Rect; + + protected static defaultOptions = { + type: LegendBase.tag, + attrs: { + width: 200, + height: 40, + padding: 0, + orient: 'horizontal', + indicator: false, + backgroundStyle: { + fill: '#dcdee2', + lineWidth: 0, + }, + title: { + content: '', + spacing: 10, + style: { + fontSize: 16, + align: 'center', + }, + formatter: (text: string) => text, + }, + }, + }; + + constructor(options: LegendBaseOptions) { + super(deepMix({}, LegendBase.defaultOptions, options)); + this.init(); + } + + attributeChangedCallback(name: string, value: any) {} + + // 获取对应状态的样式 + private getStyle(name: string | string[], status?: StyleStatus) { + const { active, disabled, checked, ...args } = get(this.attributes, name); + // 返回默认样式 + if (!status) return args; + return get(this.attributes, name)?.[status] || {}; + } + + // 获取padding + private getPadding() { + const { padding } = this.attributes; + if (isNumber) { + return new Array(4).fill(padding); + } + return padding; + } + + // 获取容器内可用空间 + private getAvailableSpace() { + const { width, height } = this.attributes; + const [top, right, bottom, left] = this.getPadding(); + return { + x: left, + y: top, + width: width - (left + right), + height: height - (top + bottom), + }; + } + + private getBackgroundAttrs() { + return { + ...this.getAvailableSpace(), + ...this.getStyle('backgroundStyle'), + }; + } + + // 绘制背景 + private createBackground() { + this.backgroundShape = new Rect({ + name: 'background', + attrs: this.getBackgroundAttrs(), + }); + this.backgroundShape.toBack(); + this.appendChild(this.backgroundShape); + } + + // 绘制标题 + private createTitle() {} +} From f3cfd0acc76459b28bfdbcf70321cf3532ce396a Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Thu, 15 Jul 2021 19:33:03 +0800 Subject: [PATCH 03/26] =?UTF-8?q?feat(legend):=20=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E4=BA=86=E8=BF=9E=E7=BB=AD=E5=9B=BE=E4=BE=8B=E5=92=8C=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E5=9B=BE=E4=BE=8B=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/index.ts | 2 +- src/ui/legend/category-item.ts | 48 +++++++++++ src/ui/legend/category.ts | 144 ++++++++++++++++++++++++++++++++ src/ui/legend/continuous.ts | 148 +++++++++++++++++++++++++++++++++ src/ui/legend/index.ts | 7 +- 5 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 src/ui/legend/category-item.ts create mode 100644 src/ui/legend/category.ts create mode 100644 src/ui/legend/continuous.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index 25b74eedc..678398834 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -5,7 +5,7 @@ export { Marker, MarkerOptions } from './marker'; export { Arrow, ArrowOptions } from './arrow'; export { Axis, AxisOptions } from './axis'; export { Button, ButtonOptions } from './button'; -export { Legend, LegendOptions } from './legend'; +export { Category, CategoryOptions, Continuous, ContinuousOptions } from './legend'; export { Scrollbar } from './scrollbar'; export type { ScrollbarOptions, ScrollbarAttrs } from './scrollbar'; export { Sheet, SheetOptions } from './sheet'; diff --git a/src/ui/legend/category-item.ts b/src/ui/legend/category-item.ts new file mode 100644 index 000000000..7b4a5b4d2 --- /dev/null +++ b/src/ui/legend/category-item.ts @@ -0,0 +1,48 @@ +import { Text, Rect } from '@antv/g'; +import { Marker } from '../marker'; +import { CustomElement, ShapeCfg } from '../../types'; +import { CategoryItemsCfg } from './types'; + +type CategoryItemCfg = ShapeCfg & { + attrs: CategoryItemsCfg['itemCfg']; +}; + +export default class CategoryItem extends CustomElement { + // marker + private markerShape: Marker; + + // name + private nameShape: Text; + + // value + private valueShape: Text; + + // background + private backgroundShape: Rect; + + constructor({ attrs, ...rest }: CategoryItemCfg) { + super({ type: 'categoryItem', attrs, ...rest }); + this.render(attrs); + } + + attributeChangedCallback(name: string, value: any) { + // 更新item + } + + public render(itemCfg: CategoryItemsCfg['itemCfg']) { + // render markerShape + // render nameShape + // render valueShape + // render backgroundShape + } + + /** + * 获得缩略文本 + */ + private getEllipsisName() {} + + /** + * 获得精简值 + */ + private getConciseValue() {} +} diff --git a/src/ui/legend/category.ts b/src/ui/legend/category.ts new file mode 100644 index 000000000..51f3440fd --- /dev/null +++ b/src/ui/legend/category.ts @@ -0,0 +1,144 @@ +import { deepMix } from '@antv/util'; +import { Marker } from '../marker'; +import LegendBase from './base'; +import CategoryItem from './category-item'; +import { CategoryCfg, CategoryOptions } from './types'; +import { DisplayObject } from '../../types'; + +export type { CategoryOptions }; + +export class Category extends LegendBase { + public static tag = 'Category'; + + private itemsContainer: DisplayObject; + + // 图例项 + private items: CategoryItem[]; + + // 前进按钮 + private prevNavigation: Marker; + + // 后退按钮 + private nextNavigation: Marker; + + protected static defaultOptions = { + ...LegendBase.defaultOptions, + items: { + items: [], + itemCfg: { + height: 16, + width: 40, + spacing: 10, + marker: { + symbol: 'circle', + size: 16, + style: { + fill: '#f8be4b', + lineWidth: 0, + active: { + fill: '#f3774a', + }, + }, + }, + name: { + spacing: 5, + style: { + stroke: 'gray', + fontSize: 16, + checked: { + stroke: 'black', + fontWeight: 'bold', + }, + }, + formatter: (name: string) => name, + }, + value: { + spacing: 5, + align: 'right', + style: { + stroke: 'gray', + fontSize: 16, + checked: { + stroke: 'black', + fontWeight: 'bold', + }, + }, + }, + backgroundStyle: { + fill: 'white', + opacity: 0.5, + active: { + fill: '#2c2c2c', + }, + }, + }, + }, + }; + + constructor(options: CategoryOptions) { + super(deepMix({}, Category.defaultOptions, options)); + } + + attributeChangedCallback(name: string, value: any) {} + + public init() {} + + public update(attrs: CategoryCfg) {} + + public clear() {} + + private bindEvents() { + // 图例项hover事件 + // 图例项点击事件 + } + + /** + * 创建图例项 + */ + private createItems() {} + + /** + * 获得一页图例项可用空间 + */ + private getItemsSpace() { + /** + * 情况1 按钮在上下、左右 无页码 + * ↑ + * item item item + * ← item item item → + * item item item + * ↓ + * + * 情况2 按钮在内 + * item item item + * item item <- -> + * + * item item item + * item <- 1/3 -> + * + * 情况3 按钮在外 + * <- 1/3 -> + * ↑ item item item ↑ + *1/3 item item item 1/3 + * ↓ item item item ↓ + * <- 1/3 -> + */ + } + + /** + * 计算图例布局 + */ + private calcLayout() {} + + // 创建翻页器 + private createPageNavigator() {} + + // 前翻页 + private onNavigationPrev = () => {}; + + // 后翻页 + private onNavigationNext = () => {}; + + // 设置图例项状态 + private setItemStatus() {} +} diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts new file mode 100644 index 000000000..56214679b --- /dev/null +++ b/src/ui/legend/continuous.ts @@ -0,0 +1,148 @@ +import { Path } from '@antv/g'; +import { deepMix } from '@antv/util'; +import LegendBase from './base'; +import { ContinuousCfg, ContinuousOptions } from './types'; +import { DisplayObject } from '../../types'; +import { Handle } from '../slider/handle'; +import { Pair } from '../slider/types'; + +export type { ContinuousOptions }; + +export class Continuous extends LegendBase { + public static tag = 'Continuous'; + + /** + * 结构: + * -backgroundShape + * |- railBackgroundShape + * |- rail + * |- startHandle + * |- endHandle + * |- indicator + */ + + // 色板 + private rail: DisplayObject; + + // 色板背景 形状跟随色板 + private railBackgroundShape: DisplayObject; + + // 开始滑块 + private startHandle: Handle; + + // 结束滑块 + private endHandle: Handle; + + /** + * drag事件当前选中的对象 + */ + private target: string; + + protected static defaultOptions = { + ...LegendBase.defaultOptions, + color: 'red', + label: { + style: { + stroke: 'black', + }, + spacing: 10, + formatter: (value: number) => value, + align: 'rail', + offset: [0, 0], + }, + rail: { + type: 'color', + chunked: false, + items: [], + isGradient: 'auto', + }, + // 不可滑动时隐藏手柄 + slidable: true, + handler: { + show: true, + size: 16, + spacing: 10, + icon: { + marker: 'default', + style: { + stroke: '#c5c5c5', + fill: '#fff', + lineWidth: 1, + }, + }, + text: { + style: { + fill: '#63656e', + textAlign: 'center', + textBaseline: 'middle', + }, + formatter: (value: number) => value, + }, + }, + }; + + constructor(options: ContinuousOptions) { + super(deepMix({}, Continuous.defaultOptions, options)); + } + + public init() {} + + public update(attrs: ContinuousCfg) {} + + public clear() {} + + // 设置指示器 + public setIndicator(value: number) {} + + // 获取色板属性 + private getRailAttrs() { + // 基于rail.type/chunked确定形状 + } + + // 创建色板 + private createRail() {} + + // 创建色板背景 + private createRailBackground() { + // 连续色板直接使用path绘制单个图形 + // 分块色板需绘制多个图形 + } + + // 创建手柄 + private createHandles() {} + + /** + * 绑定事件 + */ + private bindEvents() { + // 各种hover事件 + // 拖拽事件 + } + + // 开始拖拽 + private onDragStart = (target: string) => (e) => {}; + + // 拖拽 + private onDragging = (e) => {}; + + // 结束拖拽 + private onDragEnd = () => {}; + + /** + * 获取颜色 + */ + private getColor() {} + + /** + * 生成渐变色配置 + */ + private createGradientColor() {} + + /** + * 根据方向取值 + */ + private getOrientVal([x, y]: Pair) { + const { orient } = this.attributes; + return orient === 'horizontal' ? x : y; + } +} diff --git a/src/ui/legend/index.ts b/src/ui/legend/index.ts index 9c22c0b59..1b53f5be1 100644 --- a/src/ui/legend/index.ts +++ b/src/ui/legend/index.ts @@ -1,5 +1,2 @@ -import { LegendOptions } from './types'; - -export { LegendOptions }; - -export class Legend {} +export { Category, CategoryOptions } from './category'; +export { Continuous, ContinuousOptions } from './continuous'; From d6c31e0c758a7103c6c0bf3fbaccaf9e33560825 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 18 Jul 2021 16:39:03 +0800 Subject: [PATCH 04/26] =?UTF-8?q?refactor(legend):=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86type=E7=9A=84=E5=BC=95=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/base.ts | 6 +++--- src/ui/legend/category-item.ts | 6 +++--- src/ui/legend/category.ts | 4 ++-- src/ui/legend/continuous.ts | 23 ++++++++++++++++++----- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/ui/legend/base.ts b/src/ui/legend/base.ts index 8ae3881d0..b86aaec83 100644 --- a/src/ui/legend/base.ts +++ b/src/ui/legend/base.ts @@ -1,7 +1,7 @@ import { deepMix, get, isNumber } from '@antv/util'; import { Rect } from '@antv/g'; import { GUI } from '../core/gui'; -import { LegendBaseCfg, LegendBaseOptions, StyleStatus } from './types'; +import type { LegendBaseCfg, LegendBaseOptions, StyleStatus } from './types'; export default abstract class LegendBase extends GUI { public static tag = 'legendBase'; @@ -45,13 +45,13 @@ export default abstract class LegendBase extends GUI const { active, disabled, checked, ...args } = get(this.attributes, name); // 返回默认样式 if (!status) return args; - return get(this.attributes, name)?.[status] || {}; + return get(this.attributes, [name, status]) || {}; } // 获取padding private getPadding() { const { padding } = this.attributes; - if (isNumber) { + if (isNumber(padding)) { return new Array(4).fill(padding); } return padding; diff --git a/src/ui/legend/category-item.ts b/src/ui/legend/category-item.ts index 7b4a5b4d2..680f831c0 100644 --- a/src/ui/legend/category-item.ts +++ b/src/ui/legend/category-item.ts @@ -1,7 +1,7 @@ -import { Text, Rect } from '@antv/g'; +import { CustomElement, Text, Rect } from '@antv/g'; import { Marker } from '../marker'; -import { CustomElement, ShapeCfg } from '../../types'; -import { CategoryItemsCfg } from './types'; +import type { ShapeCfg } from '../../types'; +import type { CategoryItemsCfg } from './types'; type CategoryItemCfg = ShapeCfg & { attrs: CategoryItemsCfg['itemCfg']; diff --git a/src/ui/legend/category.ts b/src/ui/legend/category.ts index 51f3440fd..17c84f7e9 100644 --- a/src/ui/legend/category.ts +++ b/src/ui/legend/category.ts @@ -1,9 +1,9 @@ +import { DisplayObject } from '@antv/g'; import { deepMix } from '@antv/util'; import { Marker } from '../marker'; import LegendBase from './base'; import CategoryItem from './category-item'; -import { CategoryCfg, CategoryOptions } from './types'; -import { DisplayObject } from '../../types'; +import type { CategoryCfg, CategoryOptions } from './types'; export type { CategoryOptions }; diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts index 56214679b..d5c5fd7f6 100644 --- a/src/ui/legend/continuous.ts +++ b/src/ui/legend/continuous.ts @@ -1,10 +1,9 @@ -import { Path } from '@antv/g'; +import { DisplayObject, Path } from '@antv/g'; import { deepMix } from '@antv/util'; import LegendBase from './base'; -import { ContinuousCfg, ContinuousOptions } from './types'; -import { DisplayObject } from '../../types'; import { Handle } from '../slider/handle'; -import { Pair } from '../slider/types'; +import type { Pair } from '../slider/types'; +import type { ContinuousCfg, ContinuousOptions } from './types'; export type { ContinuousOptions }; @@ -100,7 +99,21 @@ export class Continuous extends LegendBase { } // 创建色板 - private createRail() {} + private createRail() { + // 确定绘制类型 + const { rail } = this.attributes; + const { type, chunked } = rail; + if (type === 'color') { + // 颜色映射 + } else if (type === 'size') { + // 尺寸映射 + } + if (chunked) { + // 分块连续图例 + } else { + // 连续图例 + } + } // 创建色板背景 private createRailBackground() { From 6c4179c641775ff43508a8e442adc4f40bc66e50 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Mon, 19 Jul 2021 10:34:10 +0800 Subject: [PATCH 05/26] =?UTF-8?q?refactor(legend):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E8=AE=A1=E7=AE=97=E8=89=B2=E6=9D=BFpath=E7=9A=84?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/utils.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/ui/legend/utils.ts diff --git a/src/ui/legend/utils.ts b/src/ui/legend/utils.ts new file mode 100644 index 000000000..452e5de95 --- /dev/null +++ b/src/ui/legend/utils.ts @@ -0,0 +1,7 @@ +export function createTriangleRailPath(width: number, height: number) { + return [['M', 0, height], ['L', width, height], ['L', width, 0], ['Z']]; +} + +export function createRectRailPath(width: number, height: number) { + return [['M', 0, 0], ['L', width, 0], ['L', width, height], ['L', 0, height], ['Z']]; +} From 3aa35e5da213f145d870c8439a2b42a606b6165c Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Mon, 19 Jul 2021 10:34:38 +0800 Subject: [PATCH 06/26] =?UTF-8?q?refactor(legend):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86title=E7=BB=98=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/base.ts | 62 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/src/ui/legend/base.ts b/src/ui/legend/base.ts index b86aaec83..81366f248 100644 --- a/src/ui/legend/base.ts +++ b/src/ui/legend/base.ts @@ -1,5 +1,5 @@ import { deepMix, get, isNumber } from '@antv/util'; -import { Rect } from '@antv/g'; +import { Rect, Text } from '@antv/g'; import { GUI } from '../core/gui'; import type { LegendBaseCfg, LegendBaseOptions, StyleStatus } from './types'; @@ -9,6 +9,8 @@ export default abstract class LegendBase extends GUI // background private backgroundShape: Rect; + private titleShape: Text; + protected static defaultOptions = { type: LegendBase.tag, attrs: { @@ -26,7 +28,8 @@ export default abstract class LegendBase extends GUI spacing: 10, style: { fontSize: 16, - align: 'center', + align: 'left', + textBaseline: 'top', }, formatter: (text: string) => text, }, @@ -40,6 +43,11 @@ export default abstract class LegendBase extends GUI attributeChangedCallback(name: string, value: any) {} + public init() { + this.createBackground(); + this.createTitle(); + } + // 获取对应状态的样式 private getStyle(name: string | string[], status?: StyleStatus) { const { active, disabled, checked, ...args } = get(this.attributes, name); @@ -59,13 +67,15 @@ export default abstract class LegendBase extends GUI // 获取容器内可用空间 private getAvailableSpace() { - const { width, height } = this.attributes; + // 容器大小 - padding - title + const { width, height, title } = this.attributes; + const { fontSize: lineHeight, spacing } = title; const [top, right, bottom, left] = this.getPadding(); return { x: left, - y: top, + y: top + lineHeight + spacing, width: width - (left + right), - height: height - (top + bottom), + height: height - (top + bottom) - lineHeight - spacing, }; } @@ -86,6 +96,44 @@ export default abstract class LegendBase extends GUI this.appendChild(this.backgroundShape); } - // 绘制标题 - private createTitle() {} + /** + * 创建图例标题配置 + */ + private getTitleAttrs() { + const { width, title } = this.attributes; + const { content, style, formatter } = title; + const { align, ...restStyle } = style; + + let layout: Object; + switch (align) { + case 'left': + layout = { x: 0, y: 0, textAlign: 'left' }; + break; + case 'right': + layout = { x: width, y: 0, textAlign: 'end' }; + break; + case 'center': + layout = { x: width / 2, y: 0, textAlign: 'center' }; + break; + default: + break; + } + + return { + ...restStyle, + ...layout, + text: formatter(content), + }; + } + + /** + * 创建图例标题 + */ + private createTitle() { + this.titleShape = new Text({ + name: 'title', + attrs: this.getTitleAttrs(), + }); + this.appendChild(this.titleShape); + } } From 462aa7b9e53b9e375c41fd02737d4429418c41ea Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Mon, 19 Jul 2021 14:08:18 +0800 Subject: [PATCH 07/26] =?UTF-8?q?refactor(legend):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E5=88=86=E9=A1=B5=E5=99=A8=E6=8C=89=E9=92=AE=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/category.ts | 25 +++++++++++++++++++++++++ src/ui/legend/utils.ts | 20 ++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/ui/legend/category.ts b/src/ui/legend/category.ts index 17c84f7e9..efb09d551 100644 --- a/src/ui/legend/category.ts +++ b/src/ui/legend/category.ts @@ -3,6 +3,7 @@ import { deepMix } from '@antv/util'; import { Marker } from '../marker'; import LegendBase from './base'; import CategoryItem from './category-item'; +import { leftArrow, rightArrow, upArrow, downArrow } from './utils'; import type { CategoryCfg, CategoryOptions } from './types'; export type { CategoryOptions }; @@ -73,6 +74,30 @@ export class Category extends LegendBase { }, }, }, + reverse: false, // 倒序放置图例 + pageNavigator: { + button: { + marker: (type: 'prev' | 'next', orient: 'horizontal' | 'vertical') => { + if (orient === 'horizontal') { + if (type === 'prev') { + return leftArrow; + } + return rightArrow; + } + // vertical + if (type === 'prev') { + return upArrow; + } + return downArrow; + }, + size: 12, + style: { + default: {}, + active: {}, + disabled: {}, + }, + }, + }, }; constructor(options: CategoryOptions) { diff --git a/src/ui/legend/utils.ts b/src/ui/legend/utils.ts index 452e5de95..01e03ef37 100644 --- a/src/ui/legend/utils.ts +++ b/src/ui/legend/utils.ts @@ -5,3 +5,23 @@ export function createTriangleRailPath(width: number, height: number) { export function createRectRailPath(width: number, height: number) { return [['M', 0, 0], ['L', width, 0], ['L', width, height], ['L', 0, height], ['Z']]; } + +export function leftArrow(x: number, y: number, r: number) { + const diffY = r * Math.sin((1 / 3) * Math.PI); + return [['M', x - r, y], ['L', x + r, y - diffY], ['L', x + r, y + diffY], ['Z']]; +} + +export function rightArrow(x: number, y: number, r: number) { + const diffY = r * Math.sin((1 / 3) * Math.PI); + return [['M', x + r, y], ['L', x - r, y - diffY], ['L', x - r, y + diffY], ['Z']]; +} + +export function upArrow(x: number, y: number, r: number) { + const diffY = r * Math.cos((1 / 3) * Math.PI); + return [['M', x - r, y + diffY], ['L', x, y - diffY], ['L', x + r, y + diffY], ['Z']]; +} + +export function downArrow(x: number, y: number, r: number) { + const diffY = r * Math.cos((1 / 3) * Math.PI); + return [['M', x - r, y - diffY], ['L', x + r, y - diffY], ['L', x, y + diffY], ['Z']]; +} From 3a566536d2322a4b9308a8fc3106d5db07577bf8 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Fri, 23 Jul 2021 10:59:50 +0800 Subject: [PATCH 08/26] =?UTF-8?q?chore:=20=E5=8D=87=E7=BA=A7=E4=BA=86g/g-c?= =?UTF-8?q?anvas=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 24ea8653f..ad698c3e0 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,13 @@ "GUI" ], "dependencies": { - "@antv/g": "^1.0.0-alpha.0", + "@antv/g": "^1.0.0-alpha.3", "@antv/g-base": "^0.5.9", - "@antv/g-canvas": "^1.0.0-alpha.0", - "@antv/util": "^2.0.13", + "@antv/g-canvas": "^1.0.0-alpha.3", + "@antv/g-svg": "^1.0.0-alpha.3", "@antv/path-util": "^2.0.9", "@antv/scale": "^0.4.3", + "@antv/util": "^2.0.13", "csstype": "^3.0.8", "svg-path-parser": "^1.1.0" }, From a7bd005e11306b1b060f466eee2358ed0d7275a2 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Fri, 23 Jul 2021 11:01:09 +0800 Subject: [PATCH 09/26] =?UTF-8?q?feat(legend):=20=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E4=BA=86legend=E7=9A=84util=E6=96=B9=E6=B3=95=EF=BC=8C?= =?UTF-8?q?=E4=B8=BB=E8=A6=81=E6=98=AF=E7=BB=98=E5=9B=BE=E5=8F=8A=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=BD=AC=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/utils.ts | 172 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/src/ui/legend/utils.ts b/src/ui/legend/utils.ts index 01e03ef37..fd170bb73 100644 --- a/src/ui/legend/utils.ts +++ b/src/ui/legend/utils.ts @@ -1,27 +1,191 @@ -export function createTriangleRailPath(width: number, height: number) { - return [['M', 0, height], ['L', width, height], ['L', width, 0], ['Z']]; +import type { PathCommand } from '@antv/g-base'; +import { isUndefined } from '@antv/util'; +import { DisplayObject } from '@antv/g'; +import { Marker } from '../marker'; +import { toPrecision } from '../../util'; + +/** + * 梯形色板 + * @param width 完整的宽度 + * @param height 完整高度 + * @param x 起点x坐标 + * @param y 起点y坐标 + * @param start 梯形起点 + * @param end 梯形终点 + * @returns PathCommand[] + */ +export function createTrapezoidRailPath( + width: number, + height: number, + x: number = 0, + y: number = 0, + start?: number, + end?: number +) { + const st = isUndefined(start) ? x : start; + const ed = isUndefined(end) ? x + width : end; + const slope = height / width; + + return [ + ['M', st, height], + ['L', st, slope * (width - st - x)], + ['L', ed, slope * (width - ed - x)], + ['L', ed, height], + ['Z'], + ] as PathCommand[]; +} + +/** + * 矩形色板 + * @param width 完整的宽度 + * @param height 完整高度 + * @param x 起点x坐标 + * @param y 起点y坐标 + * @param start 矩形起点 + * @param end 矩形终点 + * @returns PathCommand[] + */ +export function createRectRailPath( + width: number, + height: number, + x: number = 0, + y: number = 0, + start?: number, + end?: number +) { + const st = isUndefined(start) ? x : start; + const ed = isUndefined(end) ? x + width : end; + + return [['M', st, height], ['L', st, 0], ['L', ed, 0], ['L', ed, height], ['Z']] as PathCommand[]; +} + +/** + * 获得图形的x、y、width、height + */ +export function getShapeSpace(shape: DisplayObject) { + const bounds = shape.getBounds(); + const max = bounds.getMax(); + const min = bounds.getMin(); + return { + x: min[0], + y: min[1], + width: max[0] - min[0], + height: max[1] - min[1], + }; +} + +/** + * 根据值转换为其在rail上的偏移量 + * @param value 值 + * @param min 最小值 + * @param max 最大值 + * @param railLen rail长度 + * @param reverse 反向取值 + * @returns + */ +export function getValueOffset( + value: number, + min: number, + max: number, + railLen: number, + reverse: boolean = false +): number { + // 将value映射到 startRail, endRail + if (reverse) return (value / railLen) * (max - min); + return toPrecision(((value - min) / (max - min)) * railLen, 2); } -export function createRectRailPath(width: number, height: number) { - return [['M', 0, 0], ['L', width, 0], ['L', width, height], ['L', 0, height], ['Z']]; +/** + * 将值转换至步长tick上 + */ +export function getStepValue(value: number, step: number, min: number) { + const count = Math.round((value - min) / step); + return min + count * step; } +// 左箭头 export function leftArrow(x: number, y: number, r: number) { const diffY = r * Math.sin((1 / 3) * Math.PI); return [['M', x - r, y], ['L', x + r, y - diffY], ['L', x + r, y + diffY], ['Z']]; } +// 右箭头 export function rightArrow(x: number, y: number, r: number) { const diffY = r * Math.sin((1 / 3) * Math.PI); return [['M', x + r, y], ['L', x - r, y - diffY], ['L', x - r, y + diffY], ['Z']]; } +// 上三角 export function upArrow(x: number, y: number, r: number) { const diffY = r * Math.cos((1 / 3) * Math.PI); return [['M', x - r, y + diffY], ['L', x, y - diffY], ['L', x + r, y + diffY], ['Z']]; } +// 下三角 export function downArrow(x: number, y: number, r: number) { const diffY = r * Math.cos((1 / 3) * Math.PI); return [['M', x - r, y - diffY], ['L', x + r, y - diffY], ['L', x, y + diffY], ['Z']]; } + +export function hiddenHandle(x: number, y: number, r: number) { + // 长宽比 + const ratio = 1.4; + const diffY = ratio * r; + return [['M', x - r, y - diffY], ['L', x + r, y - diffY], ['L', x + r, y + diffY], ['L', x - r, y + diffY], ['Z']]; +} + +// 控制手柄 +const HANDLE_HEIGHT_RATIO = 1.4; +const HANDLE_TRIANGLE_RATIO = 0.4; + +// 纵向手柄 +export function verticalHandle(x: number, y: number, r: number) { + const width = r; + const height = width * HANDLE_HEIGHT_RATIO; + const halfWidth = width / 2; + const oneSixthWidth = width / 6; + const triangleX = x + height * HANDLE_TRIANGLE_RATIO; + return [ + ['M', x, y], + ['L', triangleX, y + halfWidth], + ['L', x + height, y + halfWidth], + ['L', x + height, y - halfWidth], + ['L', triangleX, y - halfWidth], + ['Z'], + // 绘制两条横线 + ['M', triangleX, y + oneSixthWidth], + ['L', x + height - 2, y + oneSixthWidth], + ['M', triangleX, y - oneSixthWidth], + ['L', x + height - 2, y - oneSixthWidth], + ]; +} + +// 横向手柄 +export function horizontalHandle(x: number, y: number, r: number) { + const width = r; + const height = width * HANDLE_HEIGHT_RATIO; + const halfWidth = width / 2; + const oneSixthWidth = width / 6; + const triangleY = y + height * HANDLE_TRIANGLE_RATIO; + return [ + ['M', x, y], + ['L', x - halfWidth, triangleY], + ['L', x - halfWidth, y + height], + ['L', x + halfWidth, y + height], + ['L', x + halfWidth, triangleY], + ['Z'], + // 绘制两条竖线 + ['M', x - oneSixthWidth, triangleY], + ['L', x - oneSixthWidth, y + height - 2], + ['M', x + oneSixthWidth, triangleY], + ['L', x + oneSixthWidth, y + height - 2], + ]; +} + +Marker.registerSymbol('leftArrow', leftArrow); +Marker.registerSymbol('rightArrow', rightArrow); +Marker.registerSymbol('upArrow', upArrow); +Marker.registerSymbol('downArrow', downArrow); +Marker.registerSymbol('hiddenHandle', hiddenHandle); +Marker.registerSymbol('verticalHandle', verticalHandle); +Marker.registerSymbol('horizontalHandle', horizontalHandle); From 65a7d542c709dcdb779d9b974e4fcb529695b040 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Fri, 23 Jul 2021 11:02:17 +0800 Subject: [PATCH 10/26] =?UTF-8?q?feat(legend-continuous):=20=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E4=BA=86=E7=BB=98=E5=88=B6=E8=BF=9E=E7=BB=AD=E5=9B=BE?= =?UTF-8?q?=E4=BE=8B=E8=89=B2=E6=9D=BF=E7=9A=84=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/rail.ts | 213 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 src/ui/legend/rail.ts diff --git a/src/ui/legend/rail.ts b/src/ui/legend/rail.ts new file mode 100644 index 000000000..1331ab881 --- /dev/null +++ b/src/ui/legend/rail.ts @@ -0,0 +1,213 @@ +import { isString } from '@antv/util'; +import { CustomElement, Group, Path } from '@antv/g'; +import type { PathCommand } from '@antv/g-base'; +import type { RailCfg as defaultCfg } from './types'; +import type { ShapeCfg } from '../../types'; +import { createTrapezoidRailPath, createRectRailPath, getValueOffset } from './utils'; + +type RailCfg = defaultCfg & { + min: number; + max: number; + start: number; + end: number; + color: string | string[]; + orient: 'horizontal' | 'vertical'; +}; + +export default class Rail extends CustomElement { + // 色板的path group + private rail: Group; + + // 背景的path group + private background: Group; + + constructor({ attrs, ...rest }: ShapeCfg & { attrs: RailCfg }) { + super({ type: 'rail', attrs, ...rest }); + this.init(); + } + + attributeChangedCallback(name: string, value: any) { + if (['type', 'chunked'].includes(name)) { + this.render(); + } else { + this.update(value); + } + if (['start', 'end'].includes(name)) { + this.updateSelection(); + } + } + + public init() { + this.rail = new Group({ + name: 'pathGroup', + id: 'railPathGroup', + }); + this.appendChild(this.rail); + this.background = new Group({ + name: 'backgroundGroup', + id: 'railBackgroundGroup', + }); + this.appendChild(this.background); + this.render(); + } + + public render() { + this.clear(); + const { width, color, backgroundColor, orient } = this.attributes; + const railPath = this.createRailPath(); + const railBackgroundPath = this.createBackgroundPath(); + + // 绘制背景 + railBackgroundPath.forEach((path) => { + this.background.appendChild( + new Path({ + name: 'background', + attrs: { + path, + fill: backgroundColor, + }, + }) + ); + }); + + railPath.forEach((path, idx) => { + // chunked的情况下,只显示start到end范围内的梯形 + this.rail.appendChild( + new Path({ + name: 'railPath', + attrs: { + path, + fill: isString(color) ? color : color[idx], + }, + }) + ); + }); + // 根据orient对railPath旋转 + if (orient === 'vertical') { + this.setOrigin(0, width); + this.translateLocal(0, -width); + // this.rotate(45); + setTimeout(() => { + this.rotate(45); + }); + } + } + + public update(railCfg: RailCfg) { + // deepMix railCfg into this.attributes + this.render(); + } + + /** + * 设置选区 + */ + public updateSelection() { + // 更新背景 + const backgroundPaths = this.createBackgroundPath(); + this.background.children.forEach((shape, index) => { + shape.attr({ + path: backgroundPaths[index], + }); + }); + } + + public clear() { + this.rail.removeChildren(); + this.background.removeChildren(); + } + + private getOrientVal(val1: T, val2: T) { + const { orient } = this.attributes; + return orient === 'horizontal' ? val1 : val2; + } + + /** + * 获取值对应的offset + */ + private getValueOffset(value: number) { + const { min, max, width, height } = this.attributes; + return getValueOffset(value, min, max, this.getOrientVal(width, height)); + } + + /** + * 生成rail path + */ + private createRailPath() { + const { width, height, type, chunked, start, end } = this.attributes; + let railPath: PathCommand[][]; + // 颜色映射 + if (chunked) { + railPath = this.createChunkPath(); + } else { + const startOffset = this.getValueOffset(start); + const endOffset = this.getValueOffset(end); + switch (type) { + case 'color': + railPath = [createRectRailPath(width, height, 0, 0, startOffset, endOffset)]; + break; + case 'size': + railPath = [createTrapezoidRailPath(width, height, 0, 0, startOffset, endOffset)]; + break; + default: + break; + } + } + return railPath; + } + + /** + * 分块连续图例下的path + */ + private createChunkPath(): PathCommand[][] { + const { width, height, min, max, type, ticks: _t } = this.attributes; + const range = max - min; + const [len, thick] = this.getOrientVal([width, height], [height, width]); + // 每块四个角的位置 + const blocksPoints = []; + + // 插入端值 + const ticks = [min, ..._t, max]; + // 块起始位置 + let prevPos = 0; + let prevThick = type === 'size' ? 0 : thick; + for (let index = 1; index < ticks.length; index += 1) { + const ratio = (ticks[index] - ticks[index - 1]) / range; + const currLen = len * ratio; + // 根据type确定形状 + const currThick = type === 'size' ? thick * ratio + prevThick : thick; + const currPos = prevPos + currLen; + blocksPoints.push([ + [prevPos, thick], + [prevPos, thick - prevThick], + [currPos, thick - currThick], + [currPos, thick], + ]); + prevPos = currPos; + prevThick = currThick; + } + const paths = []; + blocksPoints.forEach((points) => { + const path = []; + points.forEach((point, index) => { + path.push([index === 0 ? 'M' : 'L', ...point]); + }); + path.push(['Z']); + paths.push(path); + }); + return paths; + } + + /** + * 场景背景掩膜 + */ + private createBackgroundPath() { + const { width, height, min, max, start, end, type } = this.attributes; + const startOffset = this.getValueOffset(start); + const endOffset = this.getValueOffset(end); + const minOffset = this.getValueOffset(min); + const maxOffset = this.getValueOffset(max); + const creator = type === 'size' ? createTrapezoidRailPath : createRectRailPath; + + return [creator(width, height, 0, 0, minOffset, startOffset), creator(width, height, 0, 0, endOffset, maxOffset)]; + } +} From 00ab1dccecc36b30f1787181fc664aa88af9a163 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Fri, 23 Jul 2021 11:02:42 +0800 Subject: [PATCH 11/26] =?UTF-8?q?feat(legend-continuous):=20=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E4=BA=86=E7=BB=98=E5=88=B6labels=E7=9A=84=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/labels.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/ui/legend/labels.ts diff --git a/src/ui/legend/labels.ts b/src/ui/legend/labels.ts new file mode 100644 index 000000000..2d14c1b14 --- /dev/null +++ b/src/ui/legend/labels.ts @@ -0,0 +1,31 @@ +import { CustomElement, Text } from '@antv/g'; +import type { ShapeCfg } from '../../types'; + +type LinesCfg = ShapeCfg[]; + +export class Labels extends CustomElement { + constructor({ attrs, ...rest }: ShapeCfg) { + super({ type: 'lines', attrs, ...rest }); + this.render(attrs.labelsAttrs); + } + + public render(labelsAttrs: LinesCfg): void { + // 清空label + this.removeChildren(true); + // 重新绘制 + labelsAttrs.forEach((attr) => { + this.appendChild( + new Text({ + name: 'label', + attrs: attr, + }) + ); + }); + } + + attributeChangedCallback(name: string, value: any) { + if (name === 'linesCfg') { + this.render(value); + } + } +} From 84c6e13951303ec8d5a2d1e2b0e9437cc889bc33 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Fri, 23 Jul 2021 11:03:40 +0800 Subject: [PATCH 12/26] =?UTF-8?q?refactor(legend-continuous):=20=E5=B0=86d?= =?UTF-8?q?efaultOptions=E7=A7=BB=E8=87=B3=E5=A4=96=E9=83=A8constant?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/category.ts | 83 ++--------------- src/ui/legend/constant.ts | 184 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 76 deletions(-) create mode 100644 src/ui/legend/constant.ts diff --git a/src/ui/legend/category.ts b/src/ui/legend/category.ts index efb09d551..1e297a1e6 100644 --- a/src/ui/legend/category.ts +++ b/src/ui/legend/category.ts @@ -3,8 +3,8 @@ import { deepMix } from '@antv/util'; import { Marker } from '../marker'; import LegendBase from './base'; import CategoryItem from './category-item'; -import { leftArrow, rightArrow, upArrow, downArrow } from './utils'; import type { CategoryCfg, CategoryOptions } from './types'; +import { CATEGORY_DEFAULT_OPTIONS } from './constant'; export type { CategoryOptions }; @@ -23,81 +23,8 @@ export class Category extends LegendBase { private nextNavigation: Marker; protected static defaultOptions = { - ...LegendBase.defaultOptions, - items: { - items: [], - itemCfg: { - height: 16, - width: 40, - spacing: 10, - marker: { - symbol: 'circle', - size: 16, - style: { - fill: '#f8be4b', - lineWidth: 0, - active: { - fill: '#f3774a', - }, - }, - }, - name: { - spacing: 5, - style: { - stroke: 'gray', - fontSize: 16, - checked: { - stroke: 'black', - fontWeight: 'bold', - }, - }, - formatter: (name: string) => name, - }, - value: { - spacing: 5, - align: 'right', - style: { - stroke: 'gray', - fontSize: 16, - checked: { - stroke: 'black', - fontWeight: 'bold', - }, - }, - }, - backgroundStyle: { - fill: 'white', - opacity: 0.5, - active: { - fill: '#2c2c2c', - }, - }, - }, - }, - reverse: false, // 倒序放置图例 - pageNavigator: { - button: { - marker: (type: 'prev' | 'next', orient: 'horizontal' | 'vertical') => { - if (orient === 'horizontal') { - if (type === 'prev') { - return leftArrow; - } - return rightArrow; - } - // vertical - if (type === 'prev') { - return upArrow; - } - return downArrow; - }, - size: 12, - style: { - default: {}, - active: {}, - disabled: {}, - }, - }, - }, + type: Category.tag, + ...CATEGORY_DEFAULT_OPTIONS, }; constructor(options: CategoryOptions) { @@ -112,6 +39,10 @@ export class Category extends LegendBase { public clear() {} + protected createColor() { + return 'red'; + } + private bindEvents() { // 图例项hover事件 // 图例项点击事件 diff --git a/src/ui/legend/constant.ts b/src/ui/legend/constant.ts new file mode 100644 index 000000000..58408e686 --- /dev/null +++ b/src/ui/legend/constant.ts @@ -0,0 +1,184 @@ +import { deepMix } from '@antv/util'; +import { leftArrow, rightArrow, upArrow, downArrow } from './utils'; + +export const LEGEND_BASE_DEFAULT_OPTIONS = { + attrs: { + x: 0, + y: 0, + padding: 0, + orient: 'horizontal', + indicator: false, + backgroundStyle: { + default: { + fill: '#dcdee2', + lineWidth: 0, + }, + }, + title: { + content: '', + spacing: 10, + align: 'left', + style: { + fill: 'black', + fontSize: 16, + textBaseline: 'top', + }, + formatter: (text: string) => text, + }, + }, +}; + +export const CATEGORY_DEFAULT_OPTIONS = deepMix({}, LEGEND_BASE_DEFAULT_OPTIONS, { + attrs: { + type: 'category', + items: { + items: [], + itemCfg: { + height: 16, + width: 40, + spacing: 10, + marker: { + symbol: 'circle', + size: 16, + style: { + fill: '#f8be4b', + lineWidth: 0, + active: { + fill: '#f3774a', + }, + }, + }, + name: { + spacing: 5, + style: { + stroke: 'gray', + fontSize: 16, + checked: { + stroke: 'black', + fontWeight: 'bold', + }, + }, + formatter: (name: string) => name, + }, + value: { + spacing: 5, + align: 'right', + style: { + stroke: 'gray', + fontSize: 16, + checked: { + stroke: 'black', + fontWeight: 'bold', + }, + }, + }, + backgroundStyle: { + fill: 'white', + opacity: 0.5, + active: { + fill: '#2c2c2c', + }, + }, + }, + }, + reverse: false, // 倒序放置图例 + pageNavigator: { + button: { + marker: (type: 'prev' | 'next', orient: 'horizontal' | 'vertical') => { + if (orient === 'horizontal') { + if (type === 'prev') { + return leftArrow; + } + return rightArrow; + } + // vertical + if (type === 'prev') { + return upArrow; + } + return downArrow; + }, + size: 12, + style: { + default: {}, + active: {}, + disabled: {}, + }, + }, + }, + }, +}); + +export const CONTINUOUS_DEFAULT_OPTIONS = deepMix({}, LEGEND_BASE_DEFAULT_OPTIONS, { + attrs: { + type: 'continuous', + color: 'black', + label: { + style: { + fill: 'black', + textAlign: 'center', + textBaseline: 'middle', + }, + spacing: 10, + formatter: (value: number) => String(value), + align: 'rail', + }, + rail: { + width: 100, + height: 50, + type: 'color', + chunked: false, + ticks: [], + isGradient: 'auto', + backgroundColor: '#c5c5c5', + }, + // 不可滑动时隐藏手柄 + slidable: true, + handle: { + size: 12, + spacing: 10, + icon: { + marker: 'default', + style: { + stroke: '#c5c5c5', + fill: '#fff', + lineWidth: 1, + }, + }, + text: { + align: 'outside', + style: { + fill: '#63656e', + fontSize: 12, + textAlign: 'center', + textBaseline: 'middle', + }, + formatter: (value: number) => value, + }, + }, + indicator: { + size: 8, + spacing: 10, + icon: { + marker: 'circle', + style: { + stroke: 'white', + lineWidth: 2, + fillOpacity: 0, + }, + }, + text: { + align: 'inside', + style: { + fill: '#63656e', + fontSize: 12, + textAlign: 'center', + textBaseline: 'middle', + }, + formatter: (value: number) => value, + }, + }, + }, +}); + +// 步长比例 +export const STEP_RATIO = 0.01; From 36f75ad45717bcb4b10fe453420fc64fb157ef7b Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sat, 24 Jul 2021 10:27:18 +0800 Subject: [PATCH 13/26] =?UTF-8?q?refactor(legend):=20=E5=8F=98=E6=9B=B4?= =?UTF-8?q?=E4=BA=86=E6=96=B9=E6=B3=95=E4=B8=8E=E5=8F=98=E9=87=8F=E5=90=8D?= =?UTF-8?q?=EF=BC=8C=E8=B0=83=E6=95=B4=E4=BA=86=E9=BB=98=E8=AE=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/category.ts | 8 +-- src/ui/legend/constant.ts | 20 +++---- src/ui/legend/rail.ts | 26 ++++----- src/ui/legend/types.ts | 109 +++++++++++++++++++++----------------- src/ui/legend/utils.ts | 2 +- 5 files changed, 85 insertions(+), 80 deletions(-) diff --git a/src/ui/legend/category.ts b/src/ui/legend/category.ts index 1e297a1e6..76514e948 100644 --- a/src/ui/legend/category.ts +++ b/src/ui/legend/category.ts @@ -1,8 +1,8 @@ import { DisplayObject } from '@antv/g'; import { deepMix } from '@antv/util'; import { Marker } from '../marker'; -import LegendBase from './base'; -import CategoryItem from './category-item'; +import { LegendBase } from './base'; +import { CategoryItem } from './category-item'; import type { CategoryCfg, CategoryOptions } from './types'; import { CATEGORY_DEFAULT_OPTIONS } from './constant'; @@ -39,10 +39,12 @@ export class Category extends LegendBase { public clear() {} - protected createColor() { + protected getColor() { return 'red'; } + protected getBackgroundAttrs() {} + private bindEvents() { // 图例项hover事件 // 图例项点击事件 diff --git a/src/ui/legend/constant.ts b/src/ui/legend/constant.ts index 58408e686..be34ecfdd 100644 --- a/src/ui/legend/constant.ts +++ b/src/ui/legend/constant.ts @@ -157,24 +157,18 @@ export const CONTINUOUS_DEFAULT_OPTIONS = deepMix({}, LEGEND_BASE_DEFAULT_OPTION }, indicator: { size: 8, - spacing: 10, - icon: { - marker: 'circle', - style: { - stroke: 'white', - lineWidth: 2, - fillOpacity: 0, - }, + spacing: 5, + padding: 5, + backgroundStyle: { + fill: '#262626', + radius: 5, }, text: { - align: 'inside', style: { - fill: '#63656e', + fill: 'white', fontSize: 12, - textAlign: 'center', - textBaseline: 'middle', }, - formatter: (value: number) => value, + formatter: (value: number) => String(value), }, }, }, diff --git a/src/ui/legend/rail.ts b/src/ui/legend/rail.ts index 1331ab881..f3be8c26e 100644 --- a/src/ui/legend/rail.ts +++ b/src/ui/legend/rail.ts @@ -14,12 +14,12 @@ type RailCfg = defaultCfg & { orient: 'horizontal' | 'vertical'; }; -export default class Rail extends CustomElement { +export class Rail extends CustomElement { // 色板的path group - private rail: Group; + private railPathGroup: Group; // 背景的path group - private background: Group; + private backgroundPathGroup: Group; constructor({ attrs, ...rest }: ShapeCfg & { attrs: RailCfg }) { super({ type: 'rail', attrs, ...rest }); @@ -38,16 +38,16 @@ export default class Rail extends CustomElement { } public init() { - this.rail = new Group({ - name: 'pathGroup', + this.railPathGroup = new Group({ + name: 'railPathGroup', id: 'railPathGroup', }); - this.appendChild(this.rail); - this.background = new Group({ + this.appendChild(this.railPathGroup); + this.backgroundPathGroup = new Group({ name: 'backgroundGroup', id: 'railBackgroundGroup', }); - this.appendChild(this.background); + this.appendChild(this.backgroundPathGroup); this.render(); } @@ -59,7 +59,7 @@ export default class Rail extends CustomElement { // 绘制背景 railBackgroundPath.forEach((path) => { - this.background.appendChild( + this.backgroundPathGroup.appendChild( new Path({ name: 'background', attrs: { @@ -72,7 +72,7 @@ export default class Rail extends CustomElement { railPath.forEach((path, idx) => { // chunked的情况下,只显示start到end范围内的梯形 - this.rail.appendChild( + this.railPathGroup.appendChild( new Path({ name: 'railPath', attrs: { @@ -104,7 +104,7 @@ export default class Rail extends CustomElement { public updateSelection() { // 更新背景 const backgroundPaths = this.createBackgroundPath(); - this.background.children.forEach((shape, index) => { + this.backgroundPathGroup.children.forEach((shape, index) => { shape.attr({ path: backgroundPaths[index], }); @@ -112,8 +112,8 @@ export default class Rail extends CustomElement { } public clear() { - this.rail.removeChildren(); - this.background.removeChildren(); + this.railPathGroup.removeChildren(); + this.backgroundPathGroup.removeChildren(); } private getOrientVal(val1: T, val2: T) { diff --git a/src/ui/legend/types.ts b/src/ui/legend/types.ts index 6e8e2c2e5..65e19a0a2 100644 --- a/src/ui/legend/types.ts +++ b/src/ui/legend/types.ts @@ -1,40 +1,38 @@ -import { ShapeCfg, ShapeAttrs } from '../../types'; -import { MarkerAttrs } from '../marker/types'; - -// 状态样式:默认状态、hover状态、禁用状态、选择状态 -export type StyleStatus = 'active' | 'disabled' | 'checked'; -type MixAttrs = ShapeAttrs & - { - [key in StyleStatus]?: ShapeAttrs; - }; - +import type { ShapeCfg, ShapeAttrs, MixAttrs } from '../../types'; +import type { MarkerAttrs } from '../marker/types'; // marker配置 type MarkerCfg = string | MarkerAttrs['symbol']; // 色板 -type RailCfg = { +export type RailCfg = { + // 色板宽度 + width: number; + // 色板高度 + height: number; // 色板类型 - type: 'color' | 'size'; + type?: 'color' | 'size'; // 是否分块 - chunked: boolean; + chunked?: boolean; // 分块连续图例分割点 - items: number[]; + ticks?: number[]; // 是否使用渐变色 - isGradient: boolean | 'auto'; + isGradient?: boolean | 'auto'; + // 色板背景色 + backgroundColor?: string; }; // 滑动手柄 -type HandlerCfg = { - show: boolean; - size: number; - spacing: number; - icon: { - marker: MarkerCfg; - style: ShapeAttrs; +type HandleCfg = { + size?: number; + spacing?: number; + icon?: { + marker?: MarkerCfg; + style?: ShapeAttrs; }; - text: { - style: ShapeAttrs; - formatter: (value: number) => string; + text?: { + style?: ShapeAttrs; + formatter?: (value: number) => string; + align?: 'rail' | 'inside' | 'outside'; }; }; @@ -94,6 +92,8 @@ type pageNavigatorCfg = { marker: MarkerCfg | ((type: 'prev' | 'next') => MarkerCfg); // 按钮状态样式 style: MixAttrs; + // 尺寸 + size: number; }; // 页码 pagination: { @@ -103,24 +103,32 @@ type pageNavigatorCfg = { }; }; -export type LegendBaseCfg = ShapeCfg & { +export type LegendBaseCfg = ShapeCfg['attrs'] & { // 宽度 - width: number; + width?: number; // 高度 - height: number; + height?: number; // 图例内边距 - padding: number | number[]; + padding?: number | number[]; // 背景 - background: MixAttrs; + backgroundStyle?: MixAttrs; // 布局 - orient: 'horizontal' | 'vertical'; + orient?: 'horizontal' | 'vertical'; + // 标题 + title?: { + content: string; + spacing?: number; + align?: 'left' | 'center' | 'right'; + style?: ShapeAttrs; + formatter?: (text: string) => string; + }; // Legend类型 - type: 'category' | 'continuous'; + type?: 'category' | 'continuous'; // 指示器 - indicator: false | {}; + indicator?: false | {}; }; -export type LegendBaseOptions = ShapeCfg & { +export type LegendBaseOptions = { attrs: LegendBaseCfg; }; @@ -130,29 +138,30 @@ export type ContinuousCfg = LegendBaseCfg & { min: number; // 最大值 max: number; - // 选择区域 - value: [number, number]; // 色板颜色 - color: string | string[]; + color?: string | string[]; // 标签 - label: { - style: ShapeAttrs; - spacing: number; - formatter: (value: number) => number | string; - align: 'rail' | 'top' | 'bottom'; - offset: [number, number]; - }; + label: + | false + | { + style?: ShapeAttrs; + spacing?: number; + formatter?: (value: number, idx: number) => string; + align?: 'rail' | 'inside' | 'outside'; + }; // 色板配置 - rail: RailCfg; + rail?: RailCfg; // 是否可滑动 - slidable: boolean; + slidable?: boolean; + // 选择区域 + value?: [number, number]; // 滑动步长 - step: number; + step?: number; // 手柄配置 - handler: HandlerCfg; + Handle?: false | HandleCfg; }; -export type ContinuousOptions = ShapeCfg & { +export type ContinuousOptions = { attrs: ContinuousCfg; }; @@ -163,6 +172,6 @@ export type CategoryCfg = LegendBaseCfg & { pageNavigator: false | pageNavigatorCfg; }; -export type CategoryOptions = ShapeCfg & { +export type CategoryOptions = { attrs: CategoryCfg; }; diff --git a/src/ui/legend/utils.ts b/src/ui/legend/utils.ts index fd170bb73..4e67934a9 100644 --- a/src/ui/legend/utils.ts +++ b/src/ui/legend/utils.ts @@ -98,7 +98,7 @@ export function getValueOffset( /** * 将值转换至步长tick上 */ -export function getStepValue(value: number, step: number, min: number) { +export function getStepValueByValue(value: number, step: number, min: number) { const count = Math.round((value - min) / step); return min + count * step; } From 31770f60e3cf6cfcfec4d427f6829f834529bbba Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sat, 24 Jul 2021 10:30:31 +0800 Subject: [PATCH 14/26] =?UTF-8?q?refactor(legend-continuous):=20=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E4=BA=86=E8=BF=9E=E7=BB=AD=E5=9B=BE=E4=BE=8B=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81size=E3=80=81colro=E6=A8=A1=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?chunk=E5=88=86=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/base.ts | 147 +++--- src/ui/legend/continuous.ts | 965 ++++++++++++++++++++++++++++++++---- 2 files changed, 941 insertions(+), 171 deletions(-) diff --git a/src/ui/legend/base.ts b/src/ui/legend/base.ts index 81366f248..d1b35e3a7 100644 --- a/src/ui/legend/base.ts +++ b/src/ui/legend/base.ts @@ -1,139 +1,136 @@ -import { deepMix, get, isNumber } from '@antv/util'; +import { deepMix, get } from '@antv/util'; import { Rect, Text } from '@antv/g'; import { GUI } from '../core/gui'; -import type { LegendBaseCfg, LegendBaseOptions, StyleStatus } from './types'; - -export default abstract class LegendBase extends GUI { +import { getStateStyle, normalPadding } from '../../util'; +import { LEGEND_BASE_DEFAULT_OPTIONS } from './constant'; +import { getShapeSpace } from './utils'; +import type { Pair } from '../slider/types'; +import type { StyleState } from '../../types'; +import type { LegendBaseCfg, LegendBaseOptions } from './types'; + +export abstract class LegendBase extends GUI { public static tag = 'legendBase'; // background - private backgroundShape: Rect; + protected backgroundShape: Rect; private titleShape: Text; protected static defaultOptions = { type: LegendBase.tag, - attrs: { - width: 200, - height: 40, - padding: 0, - orient: 'horizontal', - indicator: false, - backgroundStyle: { - fill: '#dcdee2', - lineWidth: 0, - }, - title: { - content: '', - spacing: 10, - style: { - fontSize: 16, - align: 'left', - textBaseline: 'top', - }, - formatter: (text: string) => text, - }, - }, + ...LEGEND_BASE_DEFAULT_OPTIONS, }; constructor(options: LegendBaseOptions) { super(deepMix({}, LegendBase.defaultOptions, options)); - this.init(); } attributeChangedCallback(name: string, value: any) {} public init() { - this.createBackground(); + // this.createBackground(); this.createTitle(); } + /** + * 获取颜色 + */ + protected abstract getColor(): string; + // 获取对应状态的样式 - private getStyle(name: string | string[], status?: StyleStatus) { - const { active, disabled, checked, ...args } = get(this.attributes, name); - // 返回默认样式 - if (!status) return args; - return get(this.attributes, [name, status]) || {}; + protected getStyle(name: string | string[], state?: StyleState) { + const style = get(this.attributes, name); + return getStateStyle(style, state); } // 获取padding - private getPadding() { - const { padding } = this.attributes; - if (isNumber(padding)) { - return new Array(4).fill(padding); - } - return padding; + protected getPadding(padding = this.attributes.padding) { + return normalPadding(padding); } - // 获取容器内可用空间 - private getAvailableSpace() { + // 获取容器内可用空间, 排除掉title的空间 + protected getAvailableSpace() { + // 连续图例不固定外部大小 // 容器大小 - padding - title - const { width, height, title } = this.attributes; - const { fontSize: lineHeight, spacing } = title; - const [top, right, bottom, left] = this.getPadding(); + const { title } = this.attributes; + const { spacing } = title; + const [top, , , left] = this.getPadding(); + const { height: titleHeight } = getShapeSpace(this.titleShape); + return { x: left, - y: top + lineHeight + spacing, - width: width - (left + right), - height: height - (top + bottom) - lineHeight - spacing, + y: top + titleHeight + spacing, }; } - private getBackgroundAttrs() { - return { - ...this.getAvailableSpace(), - ...this.getStyle('backgroundStyle'), - }; + /** + * 根据方向取值 + */ + public getOrientVal([x, y]: Pair) { + const { orient } = this.attributes; + return orient === 'horizontal' ? x : y; } + /** + * 背景属性 + */ + protected abstract getBackgroundAttrs(); + // 绘制背景 - private createBackground() { + protected createBackground() { this.backgroundShape = new Rect({ name: 'background', attrs: this.getBackgroundAttrs(), }); - this.backgroundShape.toBack(); this.appendChild(this.backgroundShape); + this.backgroundShape.toBack(); } /** - * 创建图例标题配置 + * 创建图例标题 */ - private getTitleAttrs() { - const { width, title } = this.attributes; - const { content, style, formatter } = title; - const { align, ...restStyle } = style; + protected createTitle() { + this.titleShape = new Text({ + name: 'title', + attrs: this.getTitleAttrs(), + }); + + this.appendChild(this.titleShape); + } + + protected adjustTitle() { + const { title } = this.attributes; + const { width } = getShapeSpace(this); + const { align } = title; + const [top, right, , left] = this.getPadding(); let layout: Object; switch (align) { case 'left': - layout = { x: 0, y: 0, textAlign: 'left' }; + layout = { x: left, y: top, textAlign: 'left' }; break; case 'right': - layout = { x: width, y: 0, textAlign: 'end' }; + layout = { x: width - left - right, y: top, textAlign: 'end' }; break; case 'center': - layout = { x: width / 2, y: 0, textAlign: 'center' }; + layout = { x: (width - left - right) / 2, y: top, textAlign: 'center' }; break; default: break; } - - return { - ...restStyle, - ...layout, - text: formatter(content), - }; + this.titleShape.attr(layout); } /** - * 创建图例标题 + * 创建图例标题配置 */ - private createTitle() { - this.titleShape = new Text({ - name: 'title', - attrs: this.getTitleAttrs(), - }); - this.appendChild(this.titleShape); + private getTitleAttrs() { + const { title } = this.attributes; + const { content, style, formatter } = title; + + return { + ...style, + text: formatter(content), + }; } } diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts index d5c5fd7f6..f1c432507 100644 --- a/src/ui/legend/continuous.ts +++ b/src/ui/legend/continuous.ts @@ -1,161 +1,934 @@ -import { DisplayObject, Path } from '@antv/g'; -import { deepMix } from '@antv/util'; -import LegendBase from './base'; -import { Handle } from '../slider/handle'; +import { Group, Rect, Text } from '@antv/g'; +import { clamp, deepMix, get, isUndefined } from '@antv/util'; +import { Rail } from './rail'; +import { Labels } from './labels'; +import { LegendBase } from './base'; +import { getShapeSpace, getValueOffset, getStepValueByValue } from './utils'; +import { CONTINUOUS_DEFAULT_OPTIONS, STEP_RATIO } from './constant'; +import { Marker } from '../marker'; +import { toPrecision } from '../../util'; import type { Pair } from '../slider/types'; +import type { MarkerAttrs } from '../marker'; import type { ContinuousCfg, ContinuousOptions } from './types'; +import type { DisplayObject, ShapeAttrs } from '../../types'; export type { ContinuousOptions }; +type HandleType = 'start' | 'end'; + export class Continuous extends LegendBase { public static tag = 'Continuous'; /** * 结构: - * -backgroundShape - * |- railBackgroundShape - * |- rail - * |- startHandle - * |- endHandle - * |- indicator + * this + * |- titleShape + * |-backgroundShape + * |- labelsShape + * |- railShape + * |- pathGroup + * |- backgroundGroup + * |- startHandle + * |- endHandle + * |- indicator */ - // 色板 - private rail: DisplayObject; + // + private labelsShape: Group; - // 色板背景 形状跟随色板 - private railBackgroundShape: DisplayObject; + // 色板 + private railShape: DisplayObject; // 开始滑块 - private startHandle: Handle; + private startHandle: Group; // 结束滑块 - private endHandle: Handle; + private endHandle: Group; + + /** + * 指示器 + */ + private indicatorShape: Group; /** - * drag事件当前选中的对象 + * 当前交互的对象 */ - private target: string; + private interTarget: string; + + /** + * 上次鼠标事件的位置 + */ + private prevValue: number; protected static defaultOptions = { - ...LegendBase.defaultOptions, - color: 'red', - label: { - style: { - stroke: 'black', - }, - spacing: 10, - formatter: (value: number) => value, - align: 'rail', - offset: [0, 0], - }, - rail: { - type: 'color', - chunked: false, - items: [], - isGradient: 'auto', - }, - // 不可滑动时隐藏手柄 - slidable: true, - handler: { - show: true, - size: 16, - spacing: 10, - icon: { - marker: 'default', - style: { - stroke: '#c5c5c5', - fill: '#fff', - lineWidth: 1, - }, - }, - text: { - style: { - fill: '#63656e', - textAlign: 'center', - textBaseline: 'middle', - }, - formatter: (value: number) => value, - }, - }, + type: Continuous.tag, + ...CONTINUOUS_DEFAULT_OPTIONS, }; constructor(options: ContinuousOptions) { super(deepMix({}, Continuous.defaultOptions, options)); + super.init(); + this.init(); } - public init() {} + public init() { + // 创建labels + this.createLabels(); + // 创建色板及其背景 + this.createRail(); + // // 创建滑动手柄 + this.createHandles(); + // 设置手柄文本 + this.setHandleText(); + // 调整布局 + this.adjustLayout(); + // 调整title + this.adjustTitle(); + // 最后再绘制背景 + this.createBackground(); + // 指示器 + this.createIndicator(); + // // 监听事件 + this.bindEvents(); + console.log(this); + } - public update(attrs: ContinuousCfg) {} + public update(attrs: ContinuousCfg) { + this.attr(deepMix({}, this.attributes, attrs)); + // 更新label + this.labelsShape.attr(this.getLabelsAttrs()); + } public clear() {} - // 设置指示器 - public setIndicator(value: number) {} + /** + * 设置指示器 + * @param value 设置的值,用于确定位置 + * @param text 可选;显示的文本,若无则通过value取值 + * @param useFormatter 是否使用formatter + * @returns + */ + public setIndicator(value: false | number, text?: string, useFormatter = true) { + // 值校验 + const { min, max, rail, indicator } = this.attributes; + const safeValue = value === false ? false : clamp(value, min, max); + if (safeValue === false) { + this.indicatorShape.hide(); + return; + } + this.indicatorShape.show(); + + const { type, width: railWidth, height: railHeight } = rail; + const { spacing } = indicator; + const offsetX = this.getValueOffset(safeValue); + // 设置指示器位置 + // type = color; + // type=size时,指示器需要贴合轨道上边缘 + const offsetY = type === 'size' ? (2 - safeValue / (max - min)) * this.getOrientVal([railHeight, railWidth]) : 0; + + this.indicatorShape?.attr( + this.getOrientVal([ + { x: offsetX, y: offsetY - spacing }, + { x: offsetY + spacing, y: offsetX }, + ]) + ); + + const formatter = get(this.attributes, ['indicator', 'text', 'formatter']); + let showText = text || safeValue; + if (useFormatter) { + showText = formatter(showText); + } + // 设置文本 + this.indicatorShape.getElementsByTagName('text')[0].attr({ + text: showText, + }); + + // 调整指示器 + this.adjustIndicator(); + } + + public getSelection() { + const { min, max, start, end } = this.attributes; + return [start || min, end || max]; + } + + /** + * 设置选区 + * @param stVal 开始值 + * @param endVal 结束值 + * @param isOffset stVal和endVal是否为偏移量 + */ + public setSelection(stVal: number, endVal: number, isOffset: boolean = false) { + const [currSt, currEnd] = this.getSelection(); + let [start, end] = [stVal, endVal]; + if (isOffset) { + // 获取当前值 + start += currSt; + end += currEnd; + } + // 值校验 + [start, end] = this.getSafetySelections(start, end); + + this.setAttribute('start', start); + this.setAttribute('end', end); + this.railShape.attr({ start, end }); + this.adjustLayout(); + this.setHandleText(); + } + + /** + * 设置Handle的文本 + */ + public setHandleText(text1?: string, text2?: string, useFormatter: boolean = true) { + const [start, end] = this.getSelection(); + let [startText, endText] = [text1 || String(start), text2 || String(end)]; + if (useFormatter) { + const formatter = + get(this.attributes, ['handle', 'text', 'formatter']) || + get(Continuous.defaultOptions, ['attrs', 'handle', 'text', 'formatter']); + [startText, endText] = [formatter(startText), formatter(endText)]; + } + this.getHandle('start', 'text').attr('text', startText); + this.getHandle('end', 'text').attr('text', endText); + } + + // TODO + // public getBounds() {} + + /** + * 获取颜色 + */ + protected getColor() { + const { color } = this.attributes; + // TODO 待G 5.0 提供渐变色方法后使用 + // // 数组颜色 + // if (isArray(color)) { + // // 生成渐变色样式 + // return color.join('-'); + // } + return color; + } + + protected getBackgroundAttrs() { + const { handle } = this.attributes; + const { width, height } = getShapeSpace(this); + const [, right, bottom, left] = this.getPadding(); + // 问就是调试工程 + const offsetX = handle ? -20 : 0; + const offsetY = handle ? -10 : 0; + return { + width: width + left + right + offsetX, + height: height + bottom + offsetY, + ...this.getStyle('backgroundStyle'), + }; + } + + /** + * 获得滑动步长 + * 未指定时默认为range的1%; + */ + private getStep(): number { + const { step, min, max } = this.attributes; + if (isUndefined(step)) { + return toPrecision((max - min) * STEP_RATIO, 0); + } + return step; + } + + /** + * 取值附近的步长刻度上的值 + */ + private getStepValueByValue(value: number): number { + const { min } = this.attributes; + return getStepValueByValue(value, this.getStep(), min); + } + + /** + * 取值所在的刻度范围 + */ + private getTickIntervalByValue(value: number) { + const [start, end] = this.getSelection(); + const ticks = get(this.attributes, ['rail', 'ticks']); + const temp = [start, ...ticks, end]; + for (let i = 1; i < temp.length; i += 1) { + const st = temp[i - 1]; + const end = temp[i]; + if (value >= st && value <= end) { + return [st, end]; + } + } + return false; + } + + /** + * 将选区调整至tick位置 + */ + private adjustSelection() { + const [start, end] = this.getSelection(); + this.setSelection(this.getStepValueByValue(start), this.getStepValueByValue(end)); + } + + /** + * 获取某个值在orient方向上的偏移量 + * reverse: 屏幕偏移量 -> 值 + */ + private getValueOffset(value: number, reverse = false): number { + const { min, max, rail } = this.attributes; + const { width: railWidth, height: railHeight } = rail; + const innerLen = this.getOrientVal([railWidth, railHeight]); + return getValueOffset(value, min, max, innerLen, reverse); + } + + private getSafetySelections(start: number, end: number, precision: number = 4): [number, number] { + const { min, max } = this.attributes; + const [prevStart, prevEnd] = this.getSelection(); + let [startVal, endVal] = [start, end]; + const range = endVal - startVal; + // 交换startVal endVal + if (startVal > endVal) { + [startVal, endVal] = [endVal, startVal]; + } + // 超出范围就全选 + if (range > max - min) { + return [min, max]; + } + + if (startVal < min) { + if (prevStart === min && prevEnd === endVal) { + return [min, endVal]; + } + return [min, range + min]; + } + if (endVal > max) { + if (prevEnd === max && prevStart === startVal) { + return [startVal, max]; + } + return [max - range, max]; + } + + // 保留小数 + return [toPrecision(startVal, precision), toPrecision(endVal, precision)]; + } + + // 获取Label属性 + private getLabelsAttrs(): ShapeAttrs[] { + const { label } = this.attributes; + // 不绘制label + if (!label) { + return []; + } + const { min, max, rail } = this.attributes; + const { style, formatter, align } = label; + const attrs = []; + // align为rail时仅显示min、max的label + if (align === 'rail') { + [min, max].forEach((value, idx) => { + attrs.push({ + x: 0, + y: 0, + text: formatter(value, idx), + ...style, + }); + }); + } else { + const ticks = [min, ...rail.ticks, max]; + ticks.forEach((value, idx) => { + attrs.push({ + x: 0, + y: 0, + text: formatter(value, idx), + ...style, + }); + }); + } + return attrs; + } + + // 创建Label + private createLabels() { + // 创建label容器 + this.labelsShape = new Labels({ + name: 'labels', + id: 'labels', + attrs: { + labelsAttrs: this.getLabelsAttrs(), + }, + }); + this.appendChild(this.labelsShape); + } // 获取色板属性 private getRailAttrs() { - // 基于rail.type/chunked确定形状 + // 直接绘制色板,布局在adjustLayout方法中进行 + const { min, max, rail, orient } = this.attributes; + const [start, end] = this.getSelection(); + const color = this.getColor(); + return { + x: 0, + y: 0, + min, + max, + start, + end, + orient, + color, + cursor: 'point', + ...rail, + }; } // 创建色板 private createRail() { // 确定绘制类型 - const { rail } = this.attributes; - const { type, chunked } = rail; - if (type === 'color') { - // 颜色映射 - } else if (type === 'size') { - // 尺寸映射 - } - if (chunked) { - // 分块连续图例 + this.railShape = new Rail({ + name: 'rail', + id: 'rail', + attrs: this.getRailAttrs(), + }); + this.appendChild(this.railShape); + } + + /** + * 获取手柄属性 + */ + private getHandleAttrs() { + const { handle } = this.attributes; + if (!handle) + return { + markerAttrs: { + size: 8, + symbol: 'hiddenHandle', + opacity: 0, + }, + textAttrs: { + text: '', + opacity: 0, + }, + }; + + const { size, icon, text } = handle; + const { style: textStyle } = text; + const { marker, style: markerStyle } = icon; + // 替换默认手柄 + const symbol = marker === 'default' ? this.getOrientVal(['horizontalHandle', 'verticalHandle']) : marker; + return { + markerAttrs: { + symbol, + size, + cursor: this.getOrientVal(['ew-resize', 'ns-resize']), + ...markerStyle, + }, + textAttrs: { + text: '', + ...textStyle, + }, + }; + } + + /** + * 创建手柄 + */ + private createHandle(handleType: HandleType) { + const { markerAttrs, textAttrs } = this.getHandleAttrs(); + const groupName = `${handleType}Handle`; + const el = new Group({ + name: groupName, + id: groupName, + }); + // 将tag挂载到rail + this.railShape.appendChild(el); + + const text = new Text({ + name: 'text', + attrs: textAttrs, + }); + el.appendChild(text); + + const icon = new Marker({ + name: 'icon', + attrs: markerAttrs as MarkerAttrs, + }); + el.appendChild(icon); + this[`${handleType}handle`] = el; + } + + // 创建手柄 + private createHandles() { + this.createHandle('start'); + this.createHandle('end'); + } + + /** + * 获得手柄、手柄内icon和text的对象 + */ + private getHandle(handleType: HandleType, subNode?: 'text' | 'icon') { + // TODO fix 需要替换成查询方式 + const handle = this.getElementById(`${handleType}Handle`); + if (subNode) { + return handle.getElementsByName(subNode)[0]; + } + return handle; + } + + private getRail(item?: string) { + if (item === 'rail') { + return this.getElementById('railPathGroup'); + } + if (item === 'background') { + return this.getElementById('railBackgroundGroup'); + } + return this.getElementById('rail'); + } + + /** + * 获得指示器配置 + */ + private getIndicatorAttrs() { + const { indicator } = this.attributes; + const { size, text, backgroundStyle } = indicator; + const { style: textStyle } = text; + return { + markerAttrs: { + size, + symbol: this.getOrientVal(['downArrow', 'leftArrow']), + fill: backgroundStyle.fill, + }, + textAttrs: { + text: '', + ...this.getOrientVal([ + { + textAlign: 'center', + textBaseline: 'middle', + }, + { + textAlign: 'left', + textBaseline: 'middle', + }, + ]), + ...textStyle, + }, + backgroundAttrs: backgroundStyle, + }; + } + + private createIndicator() { + const { markerAttrs, textAttrs, backgroundAttrs } = this.getIndicatorAttrs(); + const el = new Group({ + name: 'indicator', + id: 'indicator', + }); + // 将tag挂载到rail + this.railShape.appendChild(el); + + const text = new Text({ + name: 'text', + attrs: textAttrs, + }); + el.appendChild(text); + + // 创建 取用icon填充色 + // 位置大小在setIndicator中设置 + + const background = new Rect({ + name: 'background', + attrs: { + width: 0, + height: 0, + ...backgroundAttrs, + }, + }); + el.appendChild(background); + background.toBack(); + + // 指示器小箭头 + const icon = new Marker({ + name: `icon`, + attrs: markerAttrs as MarkerAttrs, + }); + el.appendChild(icon); + this.indicatorShape = el; + // 默认隐藏 + this.indicatorShape.hide(); + } + + /** + * 调整handle结构 + */ + private adjustHandle() { + const { x: innerX, y: innerY } = this.getAvailableSpace(); + const { rail, handle } = this.attributes; + const [start, end] = this.getSelection(); + const { height: railHeight } = rail; + + // handle为false时,取默认布局方式进行布局,但不会显示出来 + const handleCfg = handle || get(Continuous.defaultOptions, ['attrs', 'handle']); + const { spacing: handleSpacing, text: handleText } = handleCfg; + const { align: handleTextAlign } = handleText; + + this.railShape.attr({ + x: innerX, + y: innerY, + }); + // 设置Handle位置 + const startHandle = this.getHandle('start'); + const endHandle = this.getHandle('end'); + startHandle.attr({ + x: this.getValueOffset(start), + y: railHeight / 2, + }); + endHandle.attr({ + x: this.getValueOffset(end), + y: railHeight / 2, + }); + // 调整文本位置 + let handleTextStyle = {}; + if (handleTextAlign === 'rail') { + // 不做处理 + handleTextStyle = {}; + } else if (handleTextAlign === 'inside') { + handleTextStyle = { + y: -railHeight / 2 - handleSpacing, + textBaseline: 'bottom', + }; + } else if (handleTextAlign === 'outside') { + handleTextStyle = { + y: railHeight / 2 + handleSpacing, + textBaseline: 'top', + }; + } + + this.getHandle('start', 'text').attr(handleTextStyle); + this.getHandle('end', 'text').attr(handleTextStyle); + } + + /** + * 调整labels结构 + */ + private adjustLabels() { + const { min, max, label, rail, orient } = this.attributes; + // 容器内可用空间起点 + const { x: innerX, y: innerY } = this.getAvailableSpace(); + const { width: railWidth, height: railHeight, ticks: _t } = rail; + // 绘制label + const { align: labelAlign, spacing: labelSpacing } = label; + // label位置 + if (labelAlign === 'rail') { + /** + * 此时labelsShape中只包含min、max label + * 1. 设置minLabel位置 + */ + if (orient === 'horizontal') { + /** + * 0 |||||||||||||||||||||| 100 + */ + + this.labelsShape.attr({ + x: innerX, + y: innerY + railHeight / 2, + }); + // 设置左侧文本 + this.labelsShape.firstChild.attr({ textAlign: 'start' }); + + // 左侧文本的宽度 + const { width: leftTextWidth } = getShapeSpace(this.labelsShape.firstChild); + + const railStart = innerX + leftTextWidth + labelSpacing; + // 设置rail位置 + this.railShape.attr({ + x: railStart, + y: innerY, + }); + + // 设置右侧文本位置 + this.labelsShape.lastChild.attr({ + x: railStart + railWidth + labelSpacing, + y: 0, + textAlign: 'start', + }); + return; + } + /** + * else if orient === 'vertical' + * 0 + * -- + * -- + * -- + * -- + * -- + * -- + * 400 + */ + this.labelsShape.attr({ + x: railWidth / 2, + y: innerY, + }); + // 顶部文本高度 + const { height: topTextHeight } = getShapeSpace(this.labelsShape.firstChild); + this.labelsShape.firstChild.attr({ + textBaseline: 'top', + }); + // rail位置 + const railStart = innerY + topTextHeight + labelSpacing; + this.railShape.attr({ + x: innerX, + y: railStart, + }); + + // 底部文本位置 + this.labelsShape.lastChild.attr({ + x: 0, + y: railStart + railHeight + labelSpacing, + textBaseline: 'top', + }); + return; + } + + /** + * if labelAlign == 'inside' | 'outside' + * |||||||||||||||||||||| + * 0 20 40 60 80 100 + * + * -- 0 + * -- 20 + * -- 40 + * -- 60 + * -- 80 + * -- 100 + */ + if (orient === 'horizontal') { + // labelsShape 高度 + const { height: labelsHeight } = getShapeSpace(this.labelsShape); + const labelSpace = labelsHeight + labelSpacing; + + this.railShape.attr({ + x: innerX, + // label在上,rail在下 + y: innerY + (labelAlign === 'inside' ? labelSpace : 0), + }); + this.labelsShape.attr({ + x: innerX, + // label在上 + y: innerY + (labelAlign === 'inside' ? 0 : labelSpace) + 10, + }); + + // 补上min,max + const ticks = [min, ..._t, max]; + // 设置labelsShape中每个文本的位置 + this.labelsShape.children.forEach((child, idx) => { + const val = ticks[idx]; + // 通过val拿到偏移量 + + child.attr({ + x: this.getValueOffset(val), + y: 0, + textBaseline: 'top', + textAlign: (() => { + // 调整两端label位置 + if (idx === 0) return 'start'; + if (idx === ticks.length - 1) return 'end'; + return 'center'; + })(), + }); + }); + return; + } + /** + * orient === 'vertical' + */ + const a = 1; + } + + /** + * 调整指示器结构 + */ + private adjustIndicator() { + const text = this.indicatorShape.getElementsByName('text')[0]; + const background = this.indicatorShape.getElementsByName('background')[0]; + const { width, height } = getShapeSpace(text); + + const [top, right, bottom, left] = this.getPadding(get(this.attributes, ['indicator', 'padding'])); + + const leftRight = left + right; + const topBottom = top + bottom; + + // 设置背景大小 + background.attr({ + x: -width / 2 - left, + y: -height - topBottom, + width: width + leftRight, + height: height + topBottom, + }); + text.attr({ + y: -height / 2 - top, + }); + } + + /** + * 对图例进行布局 + */ + private adjustLayout() { + // if (!label && !handle) { + // // 没有label和Handle + // // 直接绘制色板即可 + // } + + const { handle, label } = this.attributes; + // 调整handle + // 显示handle时不显示label + if (handle) { + this.labelsShape?.hide(); } else { - // 连续图例 + this.labelsShape?.show(); + } + if (this.getHandle('start')) { + this.adjustHandle(); } + + // 调整labels + if (label && this.labelsShape.isVisible()) { + this.adjustLabels(); + } + + // rail位置 } - // 创建色板背景 - private createRailBackground() { - // 连续色板直接使用path绘制单个图形 - // 分块色板需绘制多个图形 + /** + * 获取鼠标、触摸事件中的指针坐标 + */ + private getEventPos(e) { + // TODO 需要区分touch和mouse事件 + return [e.x, e.y] as Pair; } - // 创建手柄 - private createHandles() {} + /** + * 事件触发的位置对应的value值 + */ + private getEventPosValue(e, limit: boolean = false) { + const { min, max } = this.attributes; + const startPos = this.getOrientVal(this.getRail().getPosition().slice(0, 2) as Pair); + const currValue = this.getOrientVal(this.getEventPos(e)); + const offset = currValue - startPos; + const value = clamp(this.getValueOffset(offset, true) + min, min, max); + return value; + } /** * 绑定事件 */ private bindEvents() { + // 如果!slidable,则不绑定事件或者事件响应不生效 // 各种hover事件 - // 拖拽事件 - } - // 开始拖拽 - private onDragStart = (target: string) => (e) => {}; + // 放置需要绑定drag事件的对象 + const dragObject = new Map(); + dragObject.set('rail', this.getElementById('railBackgroundGroup')); + dragObject.set('start', this.getHandle('start', 'icon')); + dragObject.set('end', this.getHandle('end', 'icon')); + // 绑定drag开始事件 + dragObject.forEach((obj, key) => { + obj.addEventListener('mousedown', this.onDragStart(key)); + obj.addEventListener('touchstart', this.onDragStart(key)); + }); - // 拖拽 - private onDragging = (e) => {}; + // indicator hover事件 + this.getRail('background').addEventListener('mouseenter', this.onHoverStart('rail')); + this.backgroundShape.addEventListener('mouseover', this.onHoverEnd); + } - // 结束拖拽 - private onDragEnd = () => {}; + /** + * 开始拖拽 + */ + private onDragStart = (target: string) => (e) => { + e.stopPropagation(); + const { slidable } = this.attributes; + // 关闭滑动 + if (!slidable) return; + this.onHoverEnd(); + this.interTarget = target; + this.prevValue = this.getStepValueByValue(this.getEventPosValue(e)); + this.addEventListener('mousemove', this.onDragging); + this.addEventListener('touchmove', this.onDragging); + document.addEventListener('mouseup', this.onDragEnd); + document.addEventListener('touchend', this.onDragEnd); + }; /** - * 获取颜色 + * 拖拽 */ - private getColor() {} + private onDragging = (e) => { + e.stopPropagation(); + const [start, end] = this.getSelection(); + const currValue = this.getStepValueByValue(this.getEventPosValue(e)); + const diffValue = currValue - this.prevValue; + + switch (this.interTarget) { + case 'start': + start !== currValue && this.setSelection(currValue, end); + break; + case 'end': + end !== currValue && this.setSelection(start, currValue); + break; + case 'rail': + if (diffValue !== 0) { + this.prevValue = currValue; + this.setSelection(diffValue, diffValue, true); + } + break; + default: + break; + } + }; /** - * 生成渐变色配置 + * 结束拖拽 */ - private createGradientColor() {} + private onDragEnd = () => { + this.removeEventListener('mousemove', this.onDragging); + this.removeEventListener('touchmove', this.onDragging); + document.removeEventListener('mouseup', this.onDragEnd); + document.removeEventListener('touchend', this.onDragEnd); + // 抬起时修正位置 + this.interTarget = undefined; + }; + + private onHoverStart = (target: string) => (e) => { + e.stopPropagation(); + // 如果target不为undefine,表明当前有其他事件被监听 + if (isUndefined(this.interTarget)) { + this.interTarget = target; + this.addEventListener('mousemove', this.onHovering); + this.addEventListener('touchmove', this.onHovering); + } + }; + + private onHovering = (e) => { + e.stopPropagation(); + const value = this.getEventPosValue(e); + + // chunked为true时 + if (get(this.attributes, ['rail', 'chunked'])) { + const interval = this.getTickIntervalByValue(value); + if (!interval) return; + const [st, end] = interval; + this.setIndicator(toPrecision((st + end) / 2, 0), `${st}-${end}`, false); + // 计算value并发出事件 + this.emit('onIndicated', interval); + } else { + const val = this.getStepValueByValue(value); + this.setIndicator(val); + this.emit('onIndicated', val); + } + }; /** - * 根据方向取值 + * hover结束 */ - private getOrientVal([x, y]: Pair) { - const { orient } = this.attributes; - return orient === 'horizontal' ? x : y; - } + private onHoverEnd = () => { + this.removeEventListener('mousemove', this.onHovering); + this.removeEventListener('touchmove', this.onHovering); + // 关闭指示器 + this.setIndicator(false); + // 恢复状态 + this.interTarget = undefined; + }; } From 43b7a889bcb2a39c656e44ded0226f0b12f1486f Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 01:10:58 +0800 Subject: [PATCH 15/26] =?UTF-8?q?fix(legend-continuous):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E4=BA=8B=E4=BB=B6=E4=BD=8D=E7=BD=AE=E6=8B=BE?= =?UTF-8?q?=E5=8F=96=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BA=86=E6=B2=A1=E6=9C=89?= =?UTF-8?q?handle=E7=9A=84=E6=83=85=E5=86=B5=E4=B8=8Bindicator=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/continuous.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts index f1c432507..cde983b0e 100644 --- a/src/ui/legend/continuous.ts +++ b/src/ui/legend/continuous.ts @@ -110,7 +110,7 @@ export class Continuous extends LegendBase { */ public setIndicator(value: false | number, text?: string, useFormatter = true) { // 值校验 - const { min, max, rail, indicator } = this.attributes; + const { min, max, rail, handle, indicator } = this.attributes; const safeValue = value === false ? false : clamp(value, min, max); if (safeValue === false) { this.indicatorShape.hide(); @@ -124,7 +124,10 @@ export class Continuous extends LegendBase { // 设置指示器位置 // type = color; // type=size时,指示器需要贴合轨道上边缘 - const offsetY = type === 'size' ? (2 - safeValue / (max - min)) * this.getOrientVal([railHeight, railWidth]) : 0; + // handle会影响rail高度 + const _ = handle ? 2 : 1; + const offsetY = type === 'size' ? (_ - safeValue / (max - min)) * this.getOrientVal([railHeight, railWidth]) : 0; + // const offsetY = 0; this.indicatorShape?.attr( this.getOrientVal([ @@ -797,7 +800,8 @@ export class Continuous extends LegendBase { */ private getEventPos(e) { // TODO 需要区分touch和mouse事件 - return [e.x, e.y] as Pair; + const { global } = e; + return [global.x, global.y] as Pair; } /** @@ -856,6 +860,8 @@ export class Continuous extends LegendBase { * 拖拽 */ private onDragging = (e) => { + console.log(e); + e.stopPropagation(); const [start, end] = this.getSelection(); const currValue = this.getStepValueByValue(this.getEventPosValue(e)); From 15f89d1d776fcdd14c5e748e75a7eeae67d09f44 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 01:12:34 +0800 Subject: [PATCH 16/26] =?UTF-8?q?refactor(legend-continuous):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/legend/demo/basic.ts | 39 ++++++++++++++++++++++ examples/legend/demo/chunked.ts | 52 ++++++++++++++++++++++++++++++ examples/legend/demo/color-type.ts | 40 +++++++++++++++++++++++ examples/legend/demo/meta.json | 40 +++++++++++++++++++++++ examples/legend/demo/size-type.ts | 39 ++++++++++++++++++++++ 5 files changed, 210 insertions(+) create mode 100644 examples/legend/demo/basic.ts create mode 100644 examples/legend/demo/chunked.ts create mode 100644 examples/legend/demo/color-type.ts create mode 100644 examples/legend/demo/meta.json create mode 100644 examples/legend/demo/size-type.ts diff --git a/examples/legend/demo/basic.ts b/examples/legend/demo/basic.ts new file mode 100644 index 000000000..245fb0007 --- /dev/null +++ b/examples/legend/demo/basic.ts @@ -0,0 +1,39 @@ +import { Canvas } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Continuous } from '@antv/gui'; + +const renderer = new CanvasRenderer({ + enableDirtyRectangleRenderingDebug: false, + enableAutoRendering: true, + enableDirtyRectangleRendering: true, +}); + +// @ts-ignore +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 300, + renderer, +}); + +const continuous = new Continuous({ + attrs: { + title: { + content: '连续图例', + }, + label: { + align: 'outside', + }, + rail: { + width: 300, + height: 30, + ticks: [20, 40, 60, 80], + }, + handle: false, + min: 0, + max: 100, + color: '#ef923c', + }, +}); + +canvas.appendChild(continuous); diff --git a/examples/legend/demo/chunked.ts b/examples/legend/demo/chunked.ts new file mode 100644 index 000000000..540e22985 --- /dev/null +++ b/examples/legend/demo/chunked.ts @@ -0,0 +1,52 @@ +import { Canvas } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Continuous } from '@antv/gui'; + +const renderer = new CanvasRenderer({ + enableDirtyRectangleRenderingDebug: false, + enableAutoRendering: true, + enableDirtyRectangleRendering: true, +}); + +// @ts-ignore +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 300, + renderer, +}); + +const continuous = new Continuous({ + attrs: { + x: 50, + y: 50, + title: { + content: '分块图例', + }, + padding: 10, + rail: { + width: 280, + height: 30, + type: 'size', + chunked: true, + ticks: [110, 120, 130, 140, 150, 160, 170, 180, 190], + }, + min: 100, + max: 200, + step: 10, + color: [ + '#d0e3fa', + '#acc7f6', + '#8daaf2', + '#6d8eea', + '#4d73cd', + '#325bb1', + '#5a3e75', + '#8c3c79', + '#e23455', + '#e7655b', + ], + }, +}); + +canvas.appendChild(continuous); diff --git a/examples/legend/demo/color-type.ts b/examples/legend/demo/color-type.ts new file mode 100644 index 000000000..2d8184ded --- /dev/null +++ b/examples/legend/demo/color-type.ts @@ -0,0 +1,40 @@ +import { Canvas } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Continuous } from '@antv/gui'; + +const renderer = new CanvasRenderer({ + enableDirtyRectangleRenderingDebug: false, + enableAutoRendering: true, + enableDirtyRectangleRendering: true, +}); + +// @ts-ignore +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 300, + renderer, +}); + +const continuous = new Continuous({ + attrs: { + title: { + content: '颜色类型', + }, + label: { + align: 'outside', + }, + rail: { + width: 300, + height: 30, + }, + min: 0, + max: 100, + start: 10, + end: 80, + step: 5, + color: '#6720f5', + }, +}); + +canvas.appendChild(continuous); diff --git a/examples/legend/demo/meta.json b/examples/legend/demo/meta.json new file mode 100644 index 000000000..e97982eb1 --- /dev/null +++ b/examples/legend/demo/meta.json @@ -0,0 +1,40 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "basic.ts", + "title": { + "zh": "基本连续图例", + "en": "Basic continuous legend" + }, + "screenshot": "" + }, + { + "filename": "size-type.ts", + "title": { + "zh": "尺寸类型", + "en": "Size type" + }, + "screenshot": "" + }, + { + "filename": "color-type.ts", + "title": { + "zh": "颜色类型", + "en": "Color type" + }, + "screenshot": "" + }, + { + "filename": "chunked.ts", + "title": { + "zh": "分块", + "en": "Chunked" + }, + "screenshot": "" + } + ] +} diff --git a/examples/legend/demo/size-type.ts b/examples/legend/demo/size-type.ts new file mode 100644 index 000000000..a093b8e2b --- /dev/null +++ b/examples/legend/demo/size-type.ts @@ -0,0 +1,39 @@ +import { Canvas } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Continuous } from '@antv/gui'; + +const renderer = new CanvasRenderer({ + enableDirtyRectangleRenderingDebug: false, + enableAutoRendering: true, + enableDirtyRectangleRendering: true, +}); + +// @ts-ignore +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 300, + renderer, +}); + +const continuous = new Continuous({ + attrs: { + title: { + content: '尺寸类型', + }, + label: { + align: 'outside', + }, + rail: { + type: 'size', + width: 300, + height: 30, + }, + handle: false, + min: 0, + max: 100, + color: '#f1a545', + }, +}); + +canvas.appendChild(continuous); From 3657bfa80eecd47b67a97931f80b76976a239aef Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 01:14:54 +0800 Subject: [PATCH 17/26] =?UTF-8?q?docs(legend):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/ui/legend.en.md | 1 + docs/api/ui/legend.zh.md | 89 +++++++++++++++++++++++++++++++++++++ examples/legend/API.en.md | 1 + examples/legend/API.zh.md | 1 + examples/legend/index.en.md | 4 ++ examples/legend/index.zh.md | 4 ++ 6 files changed, 100 insertions(+) create mode 100644 docs/api/ui/legend.en.md create mode 100644 docs/api/ui/legend.zh.md create mode 100644 examples/legend/API.en.md create mode 100644 examples/legend/API.zh.md create mode 100644 examples/legend/index.en.md create mode 100644 examples/legend/index.zh.md diff --git a/docs/api/ui/legend.en.md b/docs/api/ui/legend.en.md new file mode 100644 index 000000000..431b7458a --- /dev/null +++ b/docs/api/ui/legend.en.md @@ -0,0 +1 @@ +`markdown:docs/api/ui/legend.zh.md` diff --git a/docs/api/ui/legend.zh.md b/docs/api/ui/legend.zh.md new file mode 100644 index 000000000..bf7b64827 --- /dev/null +++ b/docs/api/ui/legend.zh.md @@ -0,0 +1,89 @@ +--- +title: Legend +order: 5 +--- + +# 图例 + +> 图例(legend)是图表的辅助元素,使用颜色、大小、形状区分不同的数据类型,用于图表中数据的筛选 + +## 引入 + +```ts +import { Category, Continuous } from '@antv/gui'; +``` + +### 基本配置 + +| **属性名** | **类型** | **描述** | **默认值** | +| --------------- | --------------------------------------- | -------------- | -------------- | +| padding | number \| number [] | 内边距 | `10` | +| orient | 'horizontal' \| 'vertical' | 横向、纵向模式 | `'horizontal'` | +| backgroundStyle | MixAttrs | 图例背景样式 | `[]` | +| title | TitleCfg | 图例标题配置 | `[]` | +| type | 'category' \| 'continuous' | 高度 | `[]` | + +### 连续图例配置 + +| **属性名** | **类型** | **描述** | **默认值** | +| ---------- | ---------------------------------- | ---------- | -------------- | +| min | number | 最小值 | `[]` | +| max | number | 最大值 | `[]` | +| start | number | 开始区间 | `min` | +| end | number | 结束区间 | `max` | +| color | string \| string[] | 颜色 | `[]` | +| label | false \| LabelCfg | 标签 | `[]` | +| rail | RailCfg | 色板 | `[]` | +| slidable | boolean | 是否可滑动 | `true` | +| step | number | 步长 | `(max-min)*1%` | +| handle | false \| HandleCfg | 手柄配置 | `[]` | +| indicator | false \| indicatorCfg | 指示器配置 | `[]` | + +### TitleCfg + +| **属性名** | **类型** | **描述** | **默认值** | +| ---------- | ------------------------------------------ | ------------------ | ---------- | +| content | string | 标题 | | +| spacing | number | 标题与图例元素间距 | | +| align | 'left' \| 'center' \| 'right' | 标题对齐方式 | | +| style | ShapeAttrs | 标题样式 | | +| formatter | (text:string)=>string | 标题格式化 | | + +### LabelCfg + +| **属性名** | **类型** | **描述** | **默认值** | +| ---------- | ------------------------------------------------- | -------------- | ---------- | +| style | ShapeAttrs | 标签样式 | `[]` | +| spacing | number | 标签与图例间距 | `10` | +| formatter | (value: number, idx: number)=>string | 标签文本格式化 | `[]` | +| align | 'rail' \| 'inside' \| 'outside' | 标签对齐方式 | `rail` | + +### RailCfg + +| **属性名** | **类型** | **描述** | **默认值** | +| ---------- | ------------------------------ | ---------------------------- | ---------- | +| width | number | 色板宽度 | `[]` | +| height | number | 色板高度 | `[]` | +| type | 'color' \| 'size' | 色板类型 | `color` | +| chunked | boolean | 是否分块 | `false` | +| ticks | number[] | 分块分割点(label 显示的值) | `[]` | +| isGradient | boolean \| 'auto' | 是否使用渐变色 | `auto` | + +### HandleCfg + +| **属性名** | **类型** | **描述** | **默认值** | +| ---------- | ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------- | +| size | number | 手柄大小 | `4` | +| text | Object | 手柄文本 | `{formatter: (value:number)=>string, style: ShapeAttrs, align: 'rail' \| 'inside' \| 'outside' }` | +| icon | Object | 手柄图标 | `{marker: MarkerCfg}` | +| spacing | number | 手柄文本到手柄图标的间距 | `10` | + +### IndicatorCfg + +| **属性名** | **类型** | **描述** | **默认值** | +| --------------- | ------------------------------- | -------------------- | -------------------------------------------------------- | +| size | number | 指示器大小 | `8` | +| spacing | number | 指示器文本到色板间距 | `5` | +| padding | number \| number[] | 指示器文本内边距 | `5` | +| backgroundStyle | ShapeAttrs | 指示器背景样式 | `[]` | +| text | Object | 指示器文本样式 | `{style: ShapeAttrs, formatter:(value: number)=>string}` | diff --git a/examples/legend/API.en.md b/examples/legend/API.en.md new file mode 100644 index 000000000..8a9d2b4d5 --- /dev/null +++ b/examples/legend/API.en.md @@ -0,0 +1 @@ +`markdown:docs/api/ui/continuous.en.md` diff --git a/examples/legend/API.zh.md b/examples/legend/API.zh.md new file mode 100644 index 000000000..83dc99388 --- /dev/null +++ b/examples/legend/API.zh.md @@ -0,0 +1 @@ +`markdown:docs/api/ui/continuous.zh.md` diff --git a/examples/legend/index.en.md b/examples/legend/index.en.md new file mode 100644 index 000000000..29a40e584 --- /dev/null +++ b/examples/legend/index.en.md @@ -0,0 +1,4 @@ +--- +title: Legend +order: 5 +--- diff --git a/examples/legend/index.zh.md b/examples/legend/index.zh.md new file mode 100644 index 000000000..29a40e584 --- /dev/null +++ b/examples/legend/index.zh.md @@ -0,0 +1,4 @@ +--- +title: Legend +order: 5 +--- From 46f70e2fef364f4ec09dc118455b7875fb92ec38 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 01:15:28 +0800 Subject: [PATCH 18/26] =?UTF-8?q?refactor(legend):=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86=E9=BB=98=E8=AE=A4=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/constant.ts | 18 +++++++++++++++--- src/ui/legend/types.ts | 4 ---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/ui/legend/constant.ts b/src/ui/legend/constant.ts index be34ecfdd..cb917748b 100644 --- a/src/ui/legend/constant.ts +++ b/src/ui/legend/constant.ts @@ -7,7 +7,6 @@ export const LEGEND_BASE_DEFAULT_OPTIONS = { y: 0, padding: 0, orient: 'horizontal', - indicator: false, backgroundStyle: { default: { fill: '#dcdee2', @@ -19,7 +18,8 @@ export const LEGEND_BASE_DEFAULT_OPTIONS = { spacing: 10, align: 'left', style: { - fill: 'black', + fill: 'gray', + fontWeight: 'bold', fontSize: 16, textBaseline: 'top', }, @@ -111,7 +111,19 @@ export const CATEGORY_DEFAULT_OPTIONS = deepMix({}, LEGEND_BASE_DEFAULT_OPTIONS, export const CONTINUOUS_DEFAULT_OPTIONS = deepMix({}, LEGEND_BASE_DEFAULT_OPTIONS, { attrs: { type: 'continuous', - color: 'black', + color: [ + '#d0e3fa', + '#acc7f6', + '#8daaf2', + '#6d8eea', + '#4d73cd', + '#325bb1', + '#5a3e75', + '#8c3c79', + '#e23455', + '#e7655b', + ], + padding: 10, label: { style: { fill: 'black', diff --git a/src/ui/legend/types.ts b/src/ui/legend/types.ts index 65e19a0a2..0032c4e65 100644 --- a/src/ui/legend/types.ts +++ b/src/ui/legend/types.ts @@ -104,10 +104,6 @@ type pageNavigatorCfg = { }; export type LegendBaseCfg = ShapeCfg['attrs'] & { - // 宽度 - width?: number; - // 高度 - height?: number; // 图例内边距 padding?: number | number[]; // 背景 From 69a3b75a1ad29761b83f1cdc6f3eb168a00880ec Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 09:30:51 +0800 Subject: [PATCH 19/26] =?UTF-8?q?fix(legend-continuous):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=E8=83=8C=E6=99=AF=E7=BB=98=E5=88=B6=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E4=B8=8D=E6=AD=A3=E7=A1=AE=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/rail.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/legend/rail.ts b/src/ui/legend/rail.ts index f3be8c26e..7a8856072 100644 --- a/src/ui/legend/rail.ts +++ b/src/ui/legend/rail.ts @@ -1,4 +1,4 @@ -import { isString } from '@antv/util'; +import { isString, deepMix } from '@antv/util'; import { CustomElement, Group, Path } from '@antv/g'; import type { PathCommand } from '@antv/g-base'; import type { RailCfg as defaultCfg } from './types'; @@ -95,7 +95,8 @@ export class Rail extends CustomElement { public update(railCfg: RailCfg) { // deepMix railCfg into this.attributes - this.render(); + // this.attr(deepMix({}, this.attributes, railCfg)); + // this.render(); } /** @@ -133,14 +134,14 @@ export class Rail extends CustomElement { * 生成rail path */ private createRailPath() { - const { width, height, type, chunked, start, end } = this.attributes; + const { width, height, type, chunked, min, max } = this.attributes; let railPath: PathCommand[][]; // 颜色映射 if (chunked) { railPath = this.createChunkPath(); } else { - const startOffset = this.getValueOffset(start); - const endOffset = this.getValueOffset(end); + const startOffset = this.getValueOffset(min); + const endOffset = this.getValueOffset(max); switch (type) { case 'color': railPath = [createRectRailPath(width, height, 0, 0, startOffset, endOffset)]; From afd7ecf6396b46f89ac5548532a9b28a1724f049 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 11:12:50 +0800 Subject: [PATCH 20/26] =?UTF-8?q?refactor(legend-continuous):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86update=E6=96=B9=E6=B3=95=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E4=BA=86indicator=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/continuous.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts index cde983b0e..2013ad8f6 100644 --- a/src/ui/legend/continuous.ts +++ b/src/ui/legend/continuous.ts @@ -90,13 +90,24 @@ export class Continuous extends LegendBase { this.createIndicator(); // // 监听事件 this.bindEvents(); - console.log(this); } public update(attrs: ContinuousCfg) { this.attr(deepMix({}, this.attributes, attrs)); - // 更新label - this.labelsShape.attr(this.getLabelsAttrs()); + // 更新label内容 + this.labelsShape.attr({ labelsAttrs: this.getLabelsAttrs() }); + // 更新rail + this.railShape.attr(this.getRailAttrs()); + // 更新选区 + this.setSelection(...this.getSelection()); + // 更新title内容 + this.titleShape.attr(this.getTitleAttrs()); + // 关闭指示器 + this.setIndicator(false); + // 更新布局 + this.adjustLayout(); + // 更新背景 + this.backgroundShape.attr(this.getBackgroundAttrs()); } public clear() {} @@ -125,9 +136,9 @@ export class Continuous extends LegendBase { // type = color; // type=size时,指示器需要贴合轨道上边缘 // handle会影响rail高度 - const _ = handle ? 2 : 1; - const offsetY = type === 'size' ? (_ - safeValue / (max - min)) * this.getOrientVal([railHeight, railWidth]) : 0; - // const offsetY = 0; + + const offsetY = + type === 'size' ? (1 - (safeValue - min) / (max - min)) * this.getOrientVal([railHeight, railWidth]) : 0; this.indicatorShape?.attr( this.getOrientVal([ @@ -152,7 +163,7 @@ export class Continuous extends LegendBase { public getSelection() { const { min, max, start, end } = this.attributes; - return [start || min, end || max]; + return [start || min, end || max] as [number, number]; } /** @@ -800,8 +811,8 @@ export class Continuous extends LegendBase { */ private getEventPos(e) { // TODO 需要区分touch和mouse事件 - const { global } = e; - return [global.x, global.y] as Pair; + const pos = e.screen; + return [pos.x, pos.y] as Pair; } /** @@ -860,8 +871,6 @@ export class Continuous extends LegendBase { * 拖拽 */ private onDragging = (e) => { - console.log(e); - e.stopPropagation(); const [start, end] = this.getSelection(); const currValue = this.getStepValueByValue(this.getEventPosValue(e)); From ef12929146df44718e084ee677fa3f3aa8e56b25 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 11:14:14 +0800 Subject: [PATCH 21/26] =?UTF-8?q?refactor(legend):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=BA=86=E6=88=90=E5=91=98=E5=B1=9E=E6=80=A7=E3=80=81=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E7=9A=84=E8=AE=BF=E9=97=AE=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/base.ts | 58 +++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/ui/legend/base.ts b/src/ui/legend/base.ts index d1b35e3a7..247cc818f 100644 --- a/src/ui/legend/base.ts +++ b/src/ui/legend/base.ts @@ -14,7 +14,7 @@ export abstract class LegendBase extends GUI { // background protected backgroundShape: Rect; - private titleShape: Text; + protected titleShape: Text; protected static defaultOptions = { type: LegendBase.tag, @@ -28,7 +28,6 @@ export abstract class LegendBase extends GUI { attributeChangedCallback(name: string, value: any) {} public init() { - // this.createBackground(); this.createTitle(); } @@ -37,12 +36,25 @@ export abstract class LegendBase extends GUI { */ protected abstract getColor(): string; + /** + * 背景属性 + */ + protected abstract getBackgroundAttrs(); + // 获取对应状态的样式 protected getStyle(name: string | string[], state?: StyleState) { const style = get(this.attributes, name); return getStateStyle(style, state); } + /** + * 根据方向取值 + */ + protected getOrientVal([x, y]: Pair) { + const { orient } = this.attributes; + return orient === 'horizontal' ? x : y; + } + // 获取padding protected getPadding(padding = this.attributes.padding) { return normalPadding(padding); @@ -52,8 +64,7 @@ export abstract class LegendBase extends GUI { protected getAvailableSpace() { // 连续图例不固定外部大小 // 容器大小 - padding - title - const { title } = this.attributes; - const { spacing } = title; + const spacing = get(this.attributes, ['title', 'spacing']); const [top, , , left] = this.getPadding(); const { height: titleHeight } = getShapeSpace(this.titleShape); @@ -63,19 +74,6 @@ export abstract class LegendBase extends GUI { }; } - /** - * 根据方向取值 - */ - public getOrientVal([x, y]: Pair) { - const { orient } = this.attributes; - return orient === 'horizontal' ? x : y; - } - - /** - * 背景属性 - */ - protected abstract getBackgroundAttrs(); - // 绘制背景 protected createBackground() { this.backgroundShape = new Rect({ @@ -86,6 +84,19 @@ export abstract class LegendBase extends GUI { this.backgroundShape.toBack(); } + /** + * 创建图例标题配置 + */ + protected getTitleAttrs() { + const { title } = this.attributes; + const { content, style, formatter } = title; + + return { + ...style, + text: formatter(content), + }; + } + /** * 创建图例标题 */ @@ -120,17 +131,4 @@ export abstract class LegendBase extends GUI { } this.titleShape.attr(layout); } - - /** - * 创建图例标题配置 - */ - private getTitleAttrs() { - const { title } = this.attributes; - const { content, style, formatter } = title; - - return { - ...style, - text: formatter(content), - }; - } } From 186a520102013d039ed0fe5bace38f2b37b32c75 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 11:14:54 +0800 Subject: [PATCH 22/26] =?UTF-8?q?refactor(legend):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/category-item.ts | 2 +- src/ui/legend/labels.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/legend/category-item.ts b/src/ui/legend/category-item.ts index 680f831c0..30e1ced9d 100644 --- a/src/ui/legend/category-item.ts +++ b/src/ui/legend/category-item.ts @@ -7,7 +7,7 @@ type CategoryItemCfg = ShapeCfg & { attrs: CategoryItemsCfg['itemCfg']; }; -export default class CategoryItem extends CustomElement { +export class CategoryItem extends CustomElement { // marker private markerShape: Marker; diff --git a/src/ui/legend/labels.ts b/src/ui/legend/labels.ts index 2d14c1b14..9ad6b54f1 100644 --- a/src/ui/legend/labels.ts +++ b/src/ui/legend/labels.ts @@ -1,7 +1,7 @@ import { CustomElement, Text } from '@antv/g'; import type { ShapeCfg } from '../../types'; -type LinesCfg = ShapeCfg[]; +type LabelsCfg = ShapeCfg[]; export class Labels extends CustomElement { constructor({ attrs, ...rest }: ShapeCfg) { @@ -9,7 +9,7 @@ export class Labels extends CustomElement { this.render(attrs.labelsAttrs); } - public render(labelsAttrs: LinesCfg): void { + public render(labelsAttrs: LabelsCfg): void { // 清空label this.removeChildren(true); // 重新绘制 From 2349d8e0448cd08eeb9af9e598d7e4efb218d432 Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 16:18:32 +0800 Subject: [PATCH 23/26] =?UTF-8?q?refactor(legend):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BA=86=E5=8F=98=E9=87=8F=E5=AE=9A=E4=B9=89=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E4=BA=86handle=E6=9B=B4=E6=96=B0=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/continuous.ts | 37 +++++++++++++++++++++++++++---------- src/ui/legend/labels.ts | 6 +++--- src/ui/legend/rail.ts | 22 +++++++++++----------- src/ui/legend/types.ts | 4 ++-- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts index 2013ad8f6..2f0216070 100644 --- a/src/ui/legend/continuous.ts +++ b/src/ui/legend/continuous.ts @@ -37,7 +37,7 @@ export class Continuous extends LegendBase { private labelsShape: Group; // 色板 - private railShape: DisplayObject; + private railShape: Rail; // 开始滑块 private startHandle: Group; @@ -53,7 +53,7 @@ export class Continuous extends LegendBase { /** * 当前交互的对象 */ - private interTarget: string; + private target: string; /** * 上次鼠标事件的位置 @@ -102,6 +102,10 @@ export class Continuous extends LegendBase { this.setSelection(...this.getSelection()); // 更新title内容 this.titleShape.attr(this.getTitleAttrs()); + // 更新handle + this.updateHandles(); + // 更新手柄文本 + this.setHandleText(); // 关闭指示器 this.setIndicator(false); // 更新布局 @@ -121,7 +125,7 @@ export class Continuous extends LegendBase { */ public setIndicator(value: false | number, text?: string, useFormatter = true) { // 值校验 - const { min, max, rail, handle, indicator } = this.attributes; + const { min, max, rail, indicator } = this.attributes; const safeValue = value === false ? false : clamp(value, min, max); if (safeValue === false) { this.indicatorShape.hide(); @@ -472,6 +476,19 @@ export class Continuous extends LegendBase { this.createHandle('end'); } + /** + * 更新handle + */ + updateHandles() { + const { markerAttrs, textAttrs } = this.getHandleAttrs(); + ['start', 'end'].forEach((handleType) => { + const el = this.getElementById(`${handleType}Handle`); + const icon = el.getElementsByName('icon')[0] as Marker; + icon.update(markerAttrs); + el.getElementsByName('text')[0].attr(textAttrs); + }); + } + /** * 获得手柄、手柄内icon和text的对象 */ @@ -859,7 +876,7 @@ export class Continuous extends LegendBase { // 关闭滑动 if (!slidable) return; this.onHoverEnd(); - this.interTarget = target; + this.target = target; this.prevValue = this.getStepValueByValue(this.getEventPosValue(e)); this.addEventListener('mousemove', this.onDragging); this.addEventListener('touchmove', this.onDragging); @@ -876,7 +893,7 @@ export class Continuous extends LegendBase { const currValue = this.getStepValueByValue(this.getEventPosValue(e)); const diffValue = currValue - this.prevValue; - switch (this.interTarget) { + switch (this.target) { case 'start': start !== currValue && this.setSelection(currValue, end); break; @@ -903,14 +920,14 @@ export class Continuous extends LegendBase { document.removeEventListener('mouseup', this.onDragEnd); document.removeEventListener('touchend', this.onDragEnd); // 抬起时修正位置 - this.interTarget = undefined; + this.target = undefined; }; private onHoverStart = (target: string) => (e) => { e.stopPropagation(); // 如果target不为undefine,表明当前有其他事件被监听 - if (isUndefined(this.interTarget)) { - this.interTarget = target; + if (isUndefined(this.target)) { + this.target = target; this.addEventListener('mousemove', this.onHovering); this.addEventListener('touchmove', this.onHovering); } @@ -919,7 +936,6 @@ export class Continuous extends LegendBase { private onHovering = (e) => { e.stopPropagation(); const value = this.getEventPosValue(e); - // chunked为true时 if (get(this.attributes, ['rail', 'chunked'])) { const interval = this.getTickIntervalByValue(value); @@ -931,6 +947,7 @@ export class Continuous extends LegendBase { } else { const val = this.getStepValueByValue(value); this.setIndicator(val); + // TODO 节流 this.emit('onIndicated', val); } }; @@ -944,6 +961,6 @@ export class Continuous extends LegendBase { // 关闭指示器 this.setIndicator(false); // 恢复状态 - this.interTarget = undefined; + this.target = undefined; }; } diff --git a/src/ui/legend/labels.ts b/src/ui/legend/labels.ts index 9ad6b54f1..32aef376b 100644 --- a/src/ui/legend/labels.ts +++ b/src/ui/legend/labels.ts @@ -1,7 +1,7 @@ import { CustomElement, Text } from '@antv/g'; import type { ShapeCfg } from '../../types'; -type LabelsCfg = ShapeCfg[]; +type LabelsAttrs = ShapeCfg[]; export class Labels extends CustomElement { constructor({ attrs, ...rest }: ShapeCfg) { @@ -9,7 +9,7 @@ export class Labels extends CustomElement { this.render(attrs.labelsAttrs); } - public render(labelsAttrs: LabelsCfg): void { + public render(labelsAttrs: LabelsAttrs): void { // 清空label this.removeChildren(true); // 重新绘制 @@ -24,7 +24,7 @@ export class Labels extends CustomElement { } attributeChangedCallback(name: string, value: any) { - if (name === 'linesCfg') { + if (name === 'labelsAttrs') { this.render(value); } } diff --git a/src/ui/legend/rail.ts b/src/ui/legend/rail.ts index 7a8856072..eb51267c7 100644 --- a/src/ui/legend/rail.ts +++ b/src/ui/legend/rail.ts @@ -5,13 +5,13 @@ import type { RailCfg as defaultCfg } from './types'; import type { ShapeCfg } from '../../types'; import { createTrapezoidRailPath, createRectRailPath, getValueOffset } from './utils'; -type RailCfg = defaultCfg & { - min: number; - max: number; - start: number; - end: number; - color: string | string[]; - orient: 'horizontal' | 'vertical'; +type RailAttrs = defaultCfg & { + min?: number; + max?: number; + start?: number; + end?: number; + color?: string | string[]; + orient?: 'horizontal' | 'vertical'; }; export class Rail extends CustomElement { @@ -21,7 +21,7 @@ export class Rail extends CustomElement { // 背景的path group private backgroundPathGroup: Group; - constructor({ attrs, ...rest }: ShapeCfg & { attrs: RailCfg }) { + constructor({ attrs, ...rest }: ShapeCfg & { attrs: RailAttrs }) { super({ type: 'rail', attrs, ...rest }); this.init(); } @@ -93,9 +93,9 @@ export class Rail extends CustomElement { } } - public update(railCfg: RailCfg) { - // deepMix railCfg into this.attributes - // this.attr(deepMix({}, this.attributes, railCfg)); + public update(railAttrs: RailAttrs) { + // deepMix railAttrs into this.attributes + // this.attr(deepMix({}, this.attributes, railAttrs)); // this.render(); } diff --git a/src/ui/legend/types.ts b/src/ui/legend/types.ts index 0032c4e65..e44f79d8b 100644 --- a/src/ui/legend/types.ts +++ b/src/ui/legend/types.ts @@ -6,9 +6,9 @@ type MarkerCfg = string | MarkerAttrs['symbol']; // 色板 export type RailCfg = { // 色板宽度 - width: number; + width?: number; // 色板高度 - height: number; + height?: number; // 色板类型 type?: 'color' | 'size'; // 是否分块 From a184020a8b384a62e6e0f7df9294eac06542804b Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 16:19:16 +0800 Subject: [PATCH 24/26] =?UTF-8?q?docs(legend):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BA=86=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/ui/legend.zh.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/api/ui/legend.zh.md b/docs/api/ui/legend.zh.md index bf7b64827..4b566e1b6 100644 --- a/docs/api/ui/legend.zh.md +++ b/docs/api/ui/legend.zh.md @@ -60,14 +60,15 @@ import { Category, Continuous } from '@antv/gui'; ### RailCfg -| **属性名** | **类型** | **描述** | **默认值** | -| ---------- | ------------------------------ | ---------------------------- | ---------- | -| width | number | 色板宽度 | `[]` | -| height | number | 色板高度 | `[]` | -| type | 'color' \| 'size' | 色板类型 | `color` | -| chunked | boolean | 是否分块 | `false` | -| ticks | number[] | 分块分割点(label 显示的值) | `[]` | -| isGradient | boolean \| 'auto' | 是否使用渐变色 | `auto` | +| **属性名** | **类型** | **描述** | **默认值** | +| --------------- | ------------------------------ | ---------------------------- | ---------- | +| width | number | 色板宽度 | `[]` | +| height | number | 色板高度 | `[]` | +| type | 'color' \| 'size' | 色板类型 | `color` | +| chunked | boolean | 是否分块 | `false` | +| ticks | number[] | 分块分割点(label 显示的值) | `[]` | +| isGradient | boolean \| 'auto' | 是否使用渐变色 | `auto` | +| backgroundColor | string | 色板背景色 | `[]` | ### HandleCfg From 85124e6c6c85124a149b2a2464c8d4c11a27af9f Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 16:58:07 +0800 Subject: [PATCH 25/26] =?UTF-8?q?test(legend-continuous):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E4=BA=86=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/unit/ui/legend/continuous-spec.ts | 244 ++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 __tests__/unit/ui/legend/continuous-spec.ts diff --git a/__tests__/unit/ui/legend/continuous-spec.ts b/__tests__/unit/ui/legend/continuous-spec.ts new file mode 100644 index 000000000..22637e242 --- /dev/null +++ b/__tests__/unit/ui/legend/continuous-spec.ts @@ -0,0 +1,244 @@ +import { Canvas } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Continuous } from '../../../../src'; +import { createDiv } from '../../../utils'; + +// const webglRenderer = new WebGLRenderer(); + +const renderer = new CanvasRenderer({ + enableDirtyRectangleRenderingDebug: false, + enableAutoRendering: true, + enableDirtyRectangleRendering: true, +}); + +const div = createDiv(); + +// @ts-ignore +const canvas = new Canvas({ + container: div, + width: 600, + height: 600, + renderer, +}); + +const continuous = new Continuous({ + attrs: { + title: { + content: '连续图例', + }, + label: { + align: 'outside', + }, + rail: { + type: 'size', + width: 300, + height: 30, + }, + min: 0, + max: 100, + color: '#f1a545', + }, +}); + +canvas.appendChild(continuous); +canvas.render(); + +describe('continuous', () => { + test('basic', async () => { + // const continuous = new Continuous({ + // attrs: { + // // x: 50, + // // y: 50, + // title: { + // content: "I'm title", + // spacing: 20, + // align: 'left', + // style: { + // fill: 'gray', + // fontSize: 16, + // fontWeight: 'bold', + // }, + // }, + // padding: 10, + // // label: false, + // label: { + // // align: 'rail', + // // align: 'outside', + // align: 'outside', + // spacing: 10, + // // formatter: (val, idx) => { + // // if (val === 100 || val === 200) { + // // return String(val); + // // } + // // return ''; + // // }, + // }, + // backgroundStyle: { + // default: { + // fill: '#f3f3f3', + // }, + // }, + // // handle: false, + // // handle: { + // // text: { + // // align: 'inside', + // // }, + // // }, + // rail: { + // width: 280, + // height: 30, + // // type: 'size', + // chunked: true, + // ticks: [110, 120, 130, 140, 150, 160, 170, 180, 190], + // backgroundColor: '#eee8d5', + // }, + // // slidable: false, + // min: 100, + // max: 200, + // // start: 110, + // // end: 190, + // step: 10, + // color: ['#d0e3fa', '#acc7f6', '#8daaf2', '#6d8eea', '#4d73cd', '#325bb1', '#5a3e75', '#8c3c79', '#e23455', '#e7655b'], + // value: [100, 200], + // }, + + // }); + + // title + expect(continuous.firstChild.attr('text')).toBe('连续图例'); + expect(continuous.getElementById('startHandle').attr('x')).toBe(0); + expect(continuous.getElementById('endHandle').attr('x')).toBe(300); + + continuous.update({ + start: 10, + end: 50, + }); + + expect(continuous.getElementById('startHandle').attr('x')).toBe(30); + expect(continuous.getElementById('endHandle').attr('x')).toBe(150); + + console.log(continuous); + }); + + test('size', async () => { + expect(continuous.getElementById('railPathGroup').children.length).toBe(1); + expect(continuous.getElementById('railPathGroup').firstChild.attr('path')[0][2]).toBe(30); + expect(continuous.getElementById('railPathGroup').firstChild.attr('path')[2][2]).toBe(0); + }); + + test('size chunked', async () => { + continuous.update({ + rail: { + chunked: true, + ticks: [20, 30, 60, 80], + }, + color: [ + '#d0e3fa', + '#acc7f6', + '#8daaf2', + '#6d8eea', + '#4d73cd', + '#325bb1', + '#5a3e75', + '#8c3c79', + '#e23455', + '#e7655b', + ], + }); + expect(continuous.getElementById('railPathGroup').children.length).toBe(5); + expect(continuous.getElementById('railPathGroup').children[0].attr('fill')).toBe('#d0e3fa'); + expect(continuous.getElementById('railPathGroup').children[1].attr('fill')).toBe('#acc7f6'); + expect(continuous.getElementById('railPathGroup').children[2].attr('fill')).toBe('#8daaf2'); + expect(continuous.getElementById('railPathGroup').children[3].attr('fill')).toBe('#6d8eea'); + expect(continuous.getElementById('railPathGroup').children[4].attr('fill')).toBe('#4d73cd'); + expect(continuous.getElementById('railPathGroup').firstChild.attr('path')[2][2]).toBe(30 - 30 * 0.2); + expect(continuous.getElementById('railPathGroup').firstChild.attr('path')[3][2]).toBe(30); + }); + + test('color', async () => { + continuous.update({ + rail: { + type: 'color', + chunked: false, + }, + }); + expect(continuous.getElementById('railPathGroup').children.length).toBe(1); + expect(continuous.getElementById('railPathGroup').children[0].attr('fill')).toBe('#d0e3fa'); + }); + + test('color chunked', async () => { + continuous.update({ + rail: { + type: 'color', + chunked: true, + }, + color: [ + '#d0e3fa', + '#acc7f6', + '#8daaf2', + '#6d8eea', + '#4d73cd', + '#325bb1', + '#5a3e75', + '#8c3c79', + '#e23455', + '#e7655b', + ], + }); + expect(continuous.getElementById('railPathGroup').children.length).toBe(5); + expect(continuous.getElementById('railPathGroup').children[0].attr('fill')).toBe('#d0e3fa'); + expect(continuous.getElementById('railPathGroup').children[1].attr('fill')).toBe('#acc7f6'); + expect(continuous.getElementById('railPathGroup').children[2].attr('fill')).toBe('#8daaf2'); + expect(continuous.getElementById('railPathGroup').children[3].attr('fill')).toBe('#6d8eea'); + expect(continuous.getElementById('railPathGroup').children[4].attr('fill')).toBe('#4d73cd'); + expect(continuous.getElementById('railPathGroup').firstChild.attr('path')[2][2]).toBe(0); + }); + + // test('vertical', async () => { + // const div = createDiv(); + + // // @ts-ignore + // const canvas = new Canvas({ + // container: div, + // width: 600, + // height: 600, + // renderer, + // }); + + // const continuous = new Continuous({ + // attrs: { + // x: 50, + // y: 0, + // title: { + // content: "I'm title", + // }, + // label: { + // align: 'rail', + // spacing: 0 + // }, + // orient: 'vertical', + // width: 100, + // height: 300, + // backgroundStyle: { + // default: { + // fill: 'gray', + // }, + // }, + // handle: false, + // rail: { + // width: 50, + // height: 200, + // // type: 'size', + // chunked: true, + // ticks: [120, 140, 160, 180], + // }, + // min: 100, + // max: 200, + // color: ['#d0e3fa', '#acc7f6', '#8daaf2', '#6d8eea', '#4d73cd', '#325bb1'], + // value: [100, 200], + // }, + // }); + // canvas.appendChild(continuous); + // canvas.render(); + // }); +}); From 6c99bd6e0b4a1413bd53c39b6789054167f1714a Mon Sep 17 00:00:00 2001 From: Aarebecca Date: Sun, 25 Jul 2021 16:58:33 +0800 Subject: [PATCH 26/26] =?UTF-8?q?refactor(legend):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E4=BA=86rail=E7=9A=84update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/legend/continuous.ts | 4 ++-- src/ui/legend/rail.ts | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/ui/legend/continuous.ts b/src/ui/legend/continuous.ts index 2f0216070..fa4f64e8e 100644 --- a/src/ui/legend/continuous.ts +++ b/src/ui/legend/continuous.ts @@ -97,7 +97,7 @@ export class Continuous extends LegendBase { // 更新label内容 this.labelsShape.attr({ labelsAttrs: this.getLabelsAttrs() }); // 更新rail - this.railShape.attr(this.getRailAttrs()); + this.railShape.update(this.getRailAttrs()); // 更新选区 this.setSelection(...this.getSelection()); // 更新title内容 @@ -189,7 +189,7 @@ export class Continuous extends LegendBase { this.setAttribute('start', start); this.setAttribute('end', end); - this.railShape.attr({ start, end }); + this.railShape.update({ start, end }); this.adjustLayout(); this.setHandleText(); } diff --git a/src/ui/legend/rail.ts b/src/ui/legend/rail.ts index eb51267c7..27fd115f2 100644 --- a/src/ui/legend/rail.ts +++ b/src/ui/legend/rail.ts @@ -27,14 +27,14 @@ export class Rail extends CustomElement { } attributeChangedCallback(name: string, value: any) { - if (['type', 'chunked'].includes(name)) { - this.render(); - } else { - this.update(value); - } - if (['start', 'end'].includes(name)) { - this.updateSelection(); - } + // if (['type', 'chunked'].includes(name)) { + // this.render(); + // } else { + // this.update(value); + // } + // if (['start', 'end'].includes(name)) { + // this.updateSelection(); + // } } public init() { @@ -95,8 +95,9 @@ export class Rail extends CustomElement { public update(railAttrs: RailAttrs) { // deepMix railAttrs into this.attributes - // this.attr(deepMix({}, this.attributes, railAttrs)); - // this.render(); + this.attr(railAttrs); + this.render(); + this.updateSelection(); } /**