feat: support a new type of markLine, which defined by type: 'type-step'

This commit is contained in:
kkxxkk2019 2023-09-12 22:30:36 +08:00
parent 62dd5fe819
commit 31105fd477
4 changed files with 356 additions and 27 deletions

View File

@ -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();

View File

@ -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];
}
);

View File

@ -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;
}

View 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)
};
}
}