mirror of
https://github.com/VisActor/VChart
synced 2024-11-22 17:26:19 +00:00
feat: support a new type of markLine, which defined by type: 'type-step'
This commit is contained in:
parent
62dd5fe819
commit
31105fd477
@ -0,0 +1,116 @@
|
||||
import type { IBarChartSpec, ILineChartSpec, IMarkLineSpec } from '../../../../src/index';
|
||||
import { default as VChart } from '../../../../src/index';
|
||||
const CONTAINER_ID = 'chart';
|
||||
|
||||
const run = () => {
|
||||
const spec: IBarChartSpec = {
|
||||
type: 'bar',
|
||||
padding: [12, 50, 12, 12],
|
||||
data: [
|
||||
{
|
||||
id: 'barData',
|
||||
values: [
|
||||
{ month: 'Monday', sales: 22 },
|
||||
{ month: 'Tuesday', sales: 13 },
|
||||
{ month: 'Wednesday', sales: 25 },
|
||||
{ month: 'Thursday', sales: 29 },
|
||||
{ month: 'Friday', sales: 38 }
|
||||
]
|
||||
}
|
||||
],
|
||||
xField: 'month',
|
||||
yField: 'sales',
|
||||
markLine: [
|
||||
{
|
||||
type: 'type-step',
|
||||
coordinates: [
|
||||
{ month: 'Monday', sales: 22 },
|
||||
{ month: 'Wednesday', sales: 25 }
|
||||
],
|
||||
connectDirection: 'top',
|
||||
expandDistance: 50,
|
||||
label: {
|
||||
formatMethod: (a, b) => {
|
||||
console.log(a, b);
|
||||
return 'sss';
|
||||
},
|
||||
labelBackground: {
|
||||
visible: true,
|
||||
style: {
|
||||
fillOpacity: 1
|
||||
}
|
||||
},
|
||||
style: {
|
||||
fill: '#000'
|
||||
}
|
||||
},
|
||||
line: {
|
||||
style: {
|
||||
lineDash: [0],
|
||||
lineWidth: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'type-step',
|
||||
coordinates: [
|
||||
{ month: 'Thursday', sales: 29 },
|
||||
{ month: 'Friday', sales: 38 }
|
||||
],
|
||||
connectDirection: 'right',
|
||||
expandDistance: 80,
|
||||
label: {
|
||||
formatMethod: (a, b) => {
|
||||
console.log(a, b);
|
||||
return 'sss';
|
||||
},
|
||||
labelBackground: {
|
||||
visible: true,
|
||||
padding: 6,
|
||||
style: {
|
||||
fillOpacity: 1,
|
||||
fill: '#fff',
|
||||
stroke: '#000',
|
||||
lineWidth: 3,
|
||||
cornerRadius: 15
|
||||
}
|
||||
},
|
||||
style: {
|
||||
fill: '#000'
|
||||
}
|
||||
},
|
||||
line: {
|
||||
multiSegment: true,
|
||||
mainSegmentIndex: 1,
|
||||
style: [
|
||||
{
|
||||
lineDash: [2, 2],
|
||||
stroke: '#000',
|
||||
lineWidth: 2
|
||||
},
|
||||
{
|
||||
stroke: '#000',
|
||||
lineWidth: 2
|
||||
},
|
||||
{
|
||||
lineDash: [2, 2],
|
||||
stroke: '#000',
|
||||
lineWidth: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
endSymbol: {
|
||||
size: 12,
|
||||
refX: -6
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const vchart = new VChart(spec, { dom: CONTAINER_ID });
|
||||
vchart.renderAsync();
|
||||
|
||||
// Just for the convenience of console debugging, DO NOT COPY!
|
||||
window['vchart'] = vchart;
|
||||
};
|
||||
run();
|
@ -6,23 +6,28 @@ import type {
|
||||
IMarkerSpec,
|
||||
IMarkerAxisSpec,
|
||||
IDataPos,
|
||||
IDataPosCallback
|
||||
IDataPosCallback,
|
||||
IMarkerLabelSpec
|
||||
} from '../../interface';
|
||||
import type { IRegressType } from '../../mark-area/interface';
|
||||
import type { IMarkLineTheme } from './theme';
|
||||
import type { ILineMarkSpec } from '../../../../typings';
|
||||
import type { IPointLike } from '@visactor/vutils';
|
||||
|
||||
export type IMarkLine = IComponent;
|
||||
|
||||
export type IMarkLineSpec = IMarkerSpec &
|
||||
(
|
||||
| IMarkLineXSpec // 标注目标:笛卡尔坐标系坐标空间
|
||||
| IMarkLineYSpec // 标注目标:笛卡尔坐标系坐标空间
|
||||
// | IMarkLineAngleSpec // TODO: 标注目标:极坐标系坐标空间
|
||||
// | IMarkLineRadiusSpec // TODO: 标注目标:极坐标系坐标空间
|
||||
| IMarkLineCoordinateSpec // 标注目标:数据元素
|
||||
| IMarkLinePositionsSpec
|
||||
) &
|
||||
IMarkLineTheme; // 标注目标:任意位置
|
||||
export type IMarkLineSpec =
|
||||
| (IMarkerSpec &
|
||||
(
|
||||
| IMarkLineXSpec // 标注目标:笛卡尔坐标系坐标空间
|
||||
| IMarkLineYSpec // 标注目标:笛卡尔坐标系坐标空间
|
||||
// | IMarkLineAngleSpec // TODO: 标注目标:极坐标系坐标空间
|
||||
// | IMarkLineRadiusSpec // TODO: 标注目标:极坐标系坐标空间
|
||||
| IMarkLineCoordinateSpec // 标注目标:数据元素
|
||||
| IMarkLinePositionsSpec
|
||||
) &
|
||||
IMarkLineTheme)
|
||||
| IStepPMarkLineSpec; // 标注目标:任意位置
|
||||
|
||||
export interface IMarkLineXSpec extends IMarkerAxisSpec {
|
||||
/**
|
||||
@ -75,3 +80,60 @@ export type IMarkLineCoordinateSpec = {
|
||||
* 指定坐标点的参考线。基于指定坐标进行参考线的绘制
|
||||
*/
|
||||
export type IMarkLinePositionsSpec = IMarkerPositionsSpec;
|
||||
|
||||
export type IStepPMarkLineSpec = IMarkerSpec & {
|
||||
/**
|
||||
* 指定辅助线的连接类型,step 的连接方式为由交替的水平线和处置线组成的
|
||||
*/
|
||||
type: 'type-step';
|
||||
/**
|
||||
* 线的链接方向
|
||||
*/
|
||||
connectDirection: 'top' | 'bottom' | 'left' | 'right';
|
||||
/**
|
||||
* 在连接方向的扩展距离
|
||||
*/
|
||||
expandDistance?: number;
|
||||
|
||||
label?: IMarkerLabelSpec;
|
||||
line?: {
|
||||
/**
|
||||
* 是否对 points 进行多段处理,默认为 false,即直接将所有的点连接成线。
|
||||
* 如果需要进行多段处理,需要将 points 属性配置为 Point[][] 类型
|
||||
* @default false
|
||||
*/
|
||||
multiSegment?: boolean;
|
||||
/**
|
||||
* 在 `multiSegment` 属性开启的前提下,用于声明那一段线段用来作为主线段,如果不声明,默认全段为主线段
|
||||
*/
|
||||
mainSegmentIndex?: number;
|
||||
/**
|
||||
* 当进行多段配置时,可以通过数组的方式传入
|
||||
*/
|
||||
style?: ILineMarkSpec | ILineMarkSpec[];
|
||||
};
|
||||
} & Omit<IMarkLineTheme, 'label' | 'line'> &
|
||||
(
|
||||
| {
|
||||
/**
|
||||
* 指定数据点的参考线。基于指定数据点进行参考线的绘制,可以对数据点进行数据处理
|
||||
*/
|
||||
coordinates: [IDataPointSpec, IDataPointSpec];
|
||||
/**
|
||||
* 数据点的处理方法。 如果不配置则按照coordinate数组直接连接成line。
|
||||
*/
|
||||
process?:
|
||||
| {
|
||||
x: IAggrType;
|
||||
}
|
||||
| {
|
||||
y: IAggrType;
|
||||
}
|
||||
| {
|
||||
xy: IRegressType; // FIXME: xy属性名称不太合理,可能需调整
|
||||
};
|
||||
}
|
||||
| {
|
||||
positions: [IPointLike, IPointLike];
|
||||
}
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataView } from '@visactor/vdataset';
|
||||
import type { IMarkLine, IMarkLineSpec, IMarkLineTheme } from './interface';
|
||||
import type { IMarkLine, IMarkLineSpec, IMarkLineTheme, IStepPMarkLineSpec } from './interface';
|
||||
import { isNil, isArray } from '../../../util';
|
||||
import type { IComponentOption } from '../../interface';
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
@ -22,7 +22,7 @@ import type { IOptionRegr } from '../../../data/transforms/regression';
|
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import { markerRegression } from '../../../data/transforms/regression';
|
||||
import { LayoutZIndex } from '../../../constant';
|
||||
import type { IRegion } from '../../../region/interface';
|
||||
import { getInsertPoints, getTextOffset } from './util';
|
||||
|
||||
export class MarkLine extends BaseMarker<IMarkLineSpec & IMarkLineTheme> implements IMarkLine {
|
||||
static type = ComponentTypeEnum.markLine;
|
||||
@ -68,7 +68,7 @@ export class MarkLine extends BaseMarker<IMarkLineSpec & IMarkLineTheme> impleme
|
||||
y: 0
|
||||
}
|
||||
],
|
||||
lineStyle: transformToGraphic(this._spec?.line.style),
|
||||
lineStyle: this._spec?.line.style as unknown as any,
|
||||
startSymbol: {
|
||||
...this._spec?.startSymbol,
|
||||
visible: this._spec.startSymbol?.visible,
|
||||
@ -101,6 +101,9 @@ export class MarkLine extends BaseMarker<IMarkLineSpec & IMarkLineTheme> impleme
|
||||
}
|
||||
|
||||
protected _markerLayout() {
|
||||
if (!this._markerComponent) {
|
||||
return;
|
||||
}
|
||||
const spec = this._spec as any;
|
||||
const data = this._markerData;
|
||||
const startRelativeSeries = this._startRelativeSeries;
|
||||
@ -130,7 +133,7 @@ export class MarkLine extends BaseMarker<IMarkLineSpec & IMarkLineTheme> impleme
|
||||
}
|
||||
|
||||
const dataPoints = data.latestData[0].latestData ? data.latestData[0].latestData : data.latestData;
|
||||
|
||||
const seriesData = this._relativeSeries.getViewData().latestData;
|
||||
const { minX, maxX, minY, maxY } = this._computeClipRange([
|
||||
startRelativeSeries.getRegion(),
|
||||
endRelativeSeries.getRegion(),
|
||||
@ -142,17 +145,66 @@ export class MarkLine extends BaseMarker<IMarkLineSpec & IMarkLineTheme> impleme
|
||||
width: maxX - minX,
|
||||
height: maxY - minY
|
||||
};
|
||||
const labelAttrs = {
|
||||
...this._markerComponent.attribute?.label,
|
||||
text: this._spec.label.formatMethod
|
||||
? this._spec.label.formatMethod(dataPoints, seriesData)
|
||||
: this._markerComponent.attribute?.label?.text
|
||||
};
|
||||
|
||||
this._markerComponent?.setAttributes({
|
||||
points: points,
|
||||
label: {
|
||||
...this._markerComponent.attribute?.label,
|
||||
text: this._spec.label.formatMethod
|
||||
? this._spec.label.formatMethod(dataPoints)
|
||||
: this._markerComponent.attribute?.label?.text
|
||||
},
|
||||
limitRect
|
||||
});
|
||||
if ((this._spec as IStepPMarkLineSpec).type === 'type-step') {
|
||||
const { multiSegment, mainSegmentIndex } = (this._spec as IStepPMarkLineSpec).line || {};
|
||||
const { connectDirection, expandDistance = 0 } = this._spec as IStepPMarkLineSpec;
|
||||
|
||||
const joinPoints = getInsertPoints(points[0], points[1], connectDirection, expandDistance);
|
||||
|
||||
let labelPositionAttrs: any;
|
||||
if (multiSegment && isValid(mainSegmentIndex)) {
|
||||
// 如果用户配置了主线段,则不进行 label 的偏移处理,直接显示在主线段中间
|
||||
labelPositionAttrs = {
|
||||
position: 'middle',
|
||||
autoRotate: false,
|
||||
refX: 0,
|
||||
refY: 0
|
||||
};
|
||||
} else {
|
||||
labelPositionAttrs = {
|
||||
position: 'start',
|
||||
autoRotate: false,
|
||||
...getTextOffset(points[0], points[1], connectDirection, expandDistance),
|
||||
efX: 0,
|
||||
refY: 0
|
||||
};
|
||||
}
|
||||
|
||||
this._markerComponent.setAttributes({
|
||||
points: multiSegment
|
||||
? [
|
||||
[joinPoints[0], joinPoints[1]],
|
||||
[joinPoints[1], joinPoints[2]],
|
||||
[joinPoints[2], joinPoints[3]]
|
||||
]
|
||||
: joinPoints,
|
||||
label: {
|
||||
...labelAttrs,
|
||||
...labelPositionAttrs,
|
||||
textStyle: {
|
||||
...this._markerComponent.attribute?.label.textStyle,
|
||||
textAlign: 'center',
|
||||
textBaseline: 'middle'
|
||||
}
|
||||
},
|
||||
limitRect,
|
||||
multiSegment,
|
||||
mainSegmentIndex
|
||||
});
|
||||
} else {
|
||||
this._markerComponent.setAttributes({
|
||||
points: points,
|
||||
label: labelAttrs,
|
||||
limitRect
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected _initDataView(): void {
|
||||
@ -203,9 +255,10 @@ export class MarkLine extends BaseMarker<IMarkLineSpec & IMarkLineTheme> impleme
|
||||
needAgggr = true;
|
||||
}
|
||||
if (spec.process && 'xy' in spec.process) {
|
||||
const { xField, yField } = relativeSeries.getSpec();
|
||||
options = {
|
||||
fieldX: relativeSeries.getSpec().xField,
|
||||
fieldY: relativeSeries.getSpec().yField
|
||||
fieldX: xField,
|
||||
fieldY: yField
|
||||
};
|
||||
needRegr = true;
|
||||
}
|
||||
|
98
packages/vchart/src/component/marker/mark-line/util.ts
Normal file
98
packages/vchart/src/component/marker/mark-line/util.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import type { IPointLike } from '@visactor/vutils';
|
||||
|
||||
export function getInsertPoints(
|
||||
start: IPointLike,
|
||||
end: IPointLike,
|
||||
direction: 'top' | 'bottom' | 'left' | 'right',
|
||||
offset: number = 0
|
||||
) {
|
||||
const points: IPointLike[] = [];
|
||||
const dy = start.y - end.y;
|
||||
const dx = start.x - end.x;
|
||||
|
||||
switch (direction) {
|
||||
case 'top':
|
||||
points.push(start);
|
||||
points.push({
|
||||
x: start.x,
|
||||
y: dy > 0 ? start.y - offset - Math.abs(dy) : start.y - offset
|
||||
});
|
||||
points.push({
|
||||
x: end.x,
|
||||
y: dy > 0 ? end.y - offset : end.y - offset - Math.abs(dy)
|
||||
});
|
||||
points.push(end);
|
||||
break;
|
||||
case 'bottom':
|
||||
points.push(start);
|
||||
points.push({ x: start.x, y: dy < 0 ? start.y + offset + Math.abs(dy) : start.y + offset });
|
||||
points.push({ x: end.x, y: dy < 0 ? end.y + offset : end.y + offset + Math.abs(dy) });
|
||||
points.push(end);
|
||||
break;
|
||||
case 'left':
|
||||
points.push(start);
|
||||
points.push({
|
||||
x: dx > 0 ? start.x - offset - Math.abs(dx) : start.x - offset,
|
||||
y: start.y
|
||||
});
|
||||
points.push({
|
||||
x: dx > 0 ? end.x - offset : end.x - offset - Math.abs(dx),
|
||||
y: end.y
|
||||
});
|
||||
points.push(end);
|
||||
break;
|
||||
case 'right':
|
||||
points.push(start);
|
||||
points.push({
|
||||
x: dx > 0 ? start.x + offset : start.x + offset + Math.abs(dx),
|
||||
y: start.y
|
||||
});
|
||||
points.push({
|
||||
x: dx > 0 ? end.x + offset + Math.abs(dx) : end.x + offset,
|
||||
y: end.y
|
||||
});
|
||||
points.push(end);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
export function getTextOffset(
|
||||
start: IPointLike,
|
||||
end: IPointLike,
|
||||
direction: 'top' | 'bottom' | 'left' | 'right',
|
||||
offset: number = 0
|
||||
) {
|
||||
const dy = start.y - end.y;
|
||||
const dx = start.x - end.x;
|
||||
|
||||
if (direction === 'bottom') {
|
||||
return {
|
||||
dx: dx > 0 ? -dx / 2 : Math.abs(dx / 2),
|
||||
dy: dy > 0 ? offset : Math.abs(dy) + offset
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
dx: dx > 0 ? -Math.abs(dx / 2) : +Math.abs(dx / 2),
|
||||
dy: dy > 0 ? -(Math.abs(dy) + offset) : -offset
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'left') {
|
||||
return {
|
||||
dx: dx > 0 ? -dx - offset : -offset,
|
||||
dy: dy > 0 ? -(dy / 2) : Math.abs(dy / 2)
|
||||
};
|
||||
}
|
||||
|
||||
if (direction === 'right') {
|
||||
return {
|
||||
dx: dx > 0 ? offset : Math.abs(dx) + offset,
|
||||
dy: dy > 0 ? -(dy / 2) : Math.abs(dy / 2)
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user