Skip to content

Commit

Permalink
Merge pull request #1748 from ag-grid/AG-11700-annotation-move
Browse files Browse the repository at this point in the history
AG-11700 Add dragging annotations from any line
  • Loading branch information
alantreadway authored Jun 13, 2024
2 parents 022a930 + 19f007e commit 88d5d60
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 79 deletions.
1 change: 1 addition & 0 deletions packages/ag-charts-community/src/sparklines-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './util/number';
export * from './util/padding';
export * from './util/sanitize';
export * from './util/value';
export * from './util/vector';
export * from './util/zip';

export { isValidDate as isDate, isFiniteNumber as isNumber, isString } from './util/type-guards';
Expand Down
44 changes: 44 additions & 0 deletions packages/ag-charts-community/src/util/vector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export const Vec2 = {
add,
sub,
equal,
fromOffset,
apply,
required,
};

interface Vec2 {
x: number;
y: number;
}

interface OffsetVec2 {
offsetX: number;
offsetY: number;
}

function add(a: Vec2, b: Vec2): Vec2 {
return { x: a.x + b.x, y: a.y + b.y };
}

function sub(a: Vec2, b: Vec2): Vec2 {
return { x: a.x - b.x, y: a.y - b.y };
}

function equal(a: Vec2, b: Vec2): boolean {
return a.x === b.x && a.y === b.y;
}

function fromOffset(a: OffsetVec2): Vec2 {
return { x: a.offsetX, y: a.offsetY };
}

function apply(a: Partial<Vec2>, b: Vec2): Vec2 {
a.x = b.x;
a.y = b.y;
return a as Vec2;
}

function required(a?: Partial<Vec2>): Vec2 {
return { x: a?.x ?? 0, y: a?.y ?? 0 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { type Direction, _ModuleSupport, _Scene, _Util } from 'ag-charts-communi
import type { AnnotationPoint } from './annotationProperties';
import type { AnnotationAxisContext, AnnotationContext, Coords, Point } from './annotationTypes';

const { Logger } = _Util;

export function validateDatumLine(
context: AnnotationContext,
datum: { start: Point; end: Point },
Expand All @@ -26,7 +28,7 @@ export function validateDatumValue(
const valid = validateDatumPointDirection(scaleDomain(), datum.value, continuous);

if (!valid && warningPrefix) {
_Util.Logger.warnOnce(`${warningPrefix}is outside the axis domain, ignoring. - value: [${datum.value}]]`);
Logger.warnOnce(`${warningPrefix}is outside the axis domain, ignoring. - value: [${datum.value}]]`);
}

return valid;
Expand All @@ -35,7 +37,7 @@ export function validateDatumValue(
export function validateDatumPoint(context: AnnotationContext, point: Point, warningPrefix?: string) {
if (point.x == null || point.y == null) {
if (warningPrefix) {
_Util.Logger.warnOnce(`${warningPrefix}requires both an [x] and [y] property, ignoring.`);
Logger.warnOnce(`${warningPrefix}requires both an [x] and [y] property, ignoring.`);
}
return false;
}
Expand All @@ -53,9 +55,7 @@ export function validateDatumPoint(context: AnnotationContext, point: Point, war
if (validX) text = 'y domain';
if (validY) text = 'x domain';
if (warningPrefix) {
_Util.Logger.warnOnce(
`${warningPrefix}is outside the ${text}, ignoring. - x: [${point.x}], y: ${point.y}]`
);
Logger.warnOnce(`${warningPrefix}is outside the ${text}, ignoring. - x: [${point.x}], y: ${point.y}]`);
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const {
Validate,
REGIONS,
} = _ModuleSupport;
const { Vec2 } = _Util;

type Constructor<T = {}> = new (...args: any[]) => T;

Expand Down Expand Up @@ -165,6 +166,7 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M
verticalAxesRegion.addListener('click', (event) => this.onAxisClick(event, REGIONS.VERTICAL_AXES), All),
seriesRegion.addListener('hover', (event) => this.onHover(event, REGIONS.SERIES), All),
seriesRegion.addListener('click', (event) => this.onClick(event, REGIONS.SERIES), All),
seriesRegion.addListener('drag-start', this.onDragStart.bind(this), Default | ZoomDrag | AnnotationsState),
seriesRegion.addListener('drag', this.onDrag.bind(this), Default | ZoomDrag | AnnotationsState),
seriesRegion.addListener('drag-end', this.onDragEnd.bind(this), All),
seriesRegion.addListener('cancel', this.onCancel.bind(this), All),
Expand Down Expand Up @@ -463,11 +465,7 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M

if (!annotationData || !context) return;

const offset = {
x: event.offsetX - (seriesRect?.x ?? 0),
y: event.offsetY - (seriesRect?.y ?? 0),
};

const offset = Vec2.sub(Vec2.fromOffset(event), Vec2.required(seriesRect));
const point = invertCoords(offset, context);
const valid = validateDatumPoint(context, point);
cursorManager.updateCursor('annotations', valid ? undefined : Cursor.NotAllowed);
Expand Down Expand Up @@ -567,10 +565,7 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M
if (!annotationData || !context) return;

const datum = annotationData?.at(-1);
const offset = {
x: event.offsetX - (seriesRect?.x ?? 0),
y: event.offsetY - (seriesRect?.y ?? 0),
};
const offset = Vec2.sub(Vec2.fromOffset(event), Vec2.required(seriesRect));
const point = invertCoords(offset, context);

const node = active != null ? annotations.nodes()[active] : undefined;
Expand All @@ -585,35 +580,59 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M
this.update();
}

private onDragStart(event: _ModuleSupport.PointerInteractionEvent<'drag-start'>) {
const { annotationData, annotations, hovered, seriesRect } = this;

if (this.isOtherElement(event)) {
return;
}

const context = this.getAnnotationContext();

if (hovered == null || annotationData == null || !this.state.is('idle') || context == null) return;

const datum = annotationData[hovered];
const node = annotations.nodes()[hovered];
const offset = Vec2.sub(Vec2.fromOffset(event), Vec2.required(seriesRect));

if (Line.is(node)) {
node.dragStart(datum, offset, context);
}

if (CrossLine.is(node)) {
node.dragStart(datum, offset, context);
}

if (DisjointChannel.is(node)) {
node.dragStart(datum, offset, context);
}

if (ParallelChannel.is(node)) {
node.dragStart(datum, offset, context);
}
}

private onDrag(event: _ModuleSupport.PointerInteractionEvent<'drag'>) {
const {
colorPicker,
state,
ctx: { domManager },
} = this;
const { offsetX, offsetY, targetElement } = event;
const { state } = this;

if (
targetElement &&
(ToolbarManager.isChildElement(domManager, targetElement) || colorPicker.isChildElement(targetElement))
) {
if (this.isOtherElement(event)) {
return;
}

// Only track pointer offset for drag + click prevention when we are placing the first point
if (state.is('start')) {
this.dragOffset = { x: offsetX, y: offsetY };
this.dragOffset = Vec2.fromOffset(event);
}

if (state.is('idle')) {
this.onClickSelecting();
this.onDragHandle(event);
this.onDragAnnotation(event);
} else {
this.onDragAdding(event);
}
}

private onDragHandle(event: _ModuleSupport.PointerInteractionEvent<'drag'>) {
private onDragAnnotation(event: _ModuleSupport.PointerInteractionEvent<'drag'>) {
const {
annotationData,
annotations,
Expand All @@ -628,32 +647,28 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M

interactionManager.pushState(InteractionState.Annotations);

const { offsetX, offsetY } = event;
const datum = annotationData[hovered];
const node = annotations.nodes()[hovered];
const offset = {
x: offsetX - (seriesRect?.x ?? 0),
y: offsetY - (seriesRect?.y ?? 0),
};
const offset = Vec2.sub(Vec2.fromOffset(event), Vec2.required(seriesRect));

cursorManager.updateCursor('annotations');

const onDragInvalid = () => this.ctx.cursorManager.updateCursor('annotations', Cursor.NotAllowed);

if (LineAnnotation.is(datum) && Line.is(node)) {
node.dragHandle(datum, offset, context, onDragInvalid);
node.drag(datum, offset, context, onDragInvalid);
}

if (CrossLineAnnotation.is(datum) && CrossLine.is(node)) {
node.dragHandle(datum, offset, context, onDragInvalid);
node.drag(datum, offset, context, onDragInvalid);
}

if (DisjointChannelAnnotation.is(datum) && DisjointChannel.is(node)) {
node.dragHandle(datum, offset, context, onDragInvalid);
node.drag(datum, offset, context, onDragInvalid);
}

if (ParallelChannelAnnotation.is(datum) && ParallelChannel.is(node)) {
node.dragHandle(datum, offset, context, onDragInvalid);
node.drag(datum, offset, context, onDragInvalid);
}

this.update();
Expand All @@ -672,13 +687,9 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M
const context = this.getAnnotationContext();
if (annotationData == null || context == null) return;

const { offsetX, offsetY } = event;
const datum = active != null ? annotationData[active] : undefined;
const node = active != null ? annotations.nodes()[active] : undefined;
const offset = {
x: offsetX - (seriesRect?.x ?? 0),
y: offsetY - (seriesRect?.y ?? 0),
};
const offset = Vec2.sub(Vec2.fromOffset(event), Vec2.required(seriesRect));

interactionManager.pushState(InteractionState.Annotations);

Expand Down Expand Up @@ -769,6 +780,17 @@ export class Annotations extends _ModuleSupport.BaseModuleInstance implements _M
}
}

private isOtherElement({ targetElement }: { targetElement?: HTMLElement }) {
const {
colorPicker,
ctx: { domManager },
} = this;

if (!targetElement) return false;

return ToolbarManager.isChildElement(domManager, targetElement) || colorPicker.isChildElement(targetElement);
}

private clear() {
this.annotationData?.splice(0, this.annotationData?.length);
this.reset();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { Direction, _Scene } from 'ag-charts-community';
import { type Direction, type _Scene, _Util } from 'ag-charts-community';

import type { AnnotationAxisContext, AnnotationContext, Coords } from '../annotationTypes';
import { invertCoords, validateDatumPoint } from '../annotationUtils';
import { convert, invertCoords, validateDatumPoint } from '../annotationUtils';
import { Annotation } from '../scenes/annotation';
import { AxisLabel } from '../scenes/axisLabel';
import { UnivariantHandle } from '../scenes/handle';
import { CollidableLine } from '../scenes/shapes';
import type { CrossLineAnnotation } from './crossLineProperties';

const { Vec2 } = _Util;

export class CrossLine extends Annotation {
static override is(value: unknown): value is CrossLine {
return Annotation.isCheck(value, 'cross-line');
Expand All @@ -22,6 +24,10 @@ export class CrossLine extends Annotation {
private readonly axisLabel = new AxisLabel();

private seriesRect?: _Scene.BBox;
private dragState?: {
offset: Coords;
middle: Coords;
};

constructor() {
super();
Expand Down Expand Up @@ -104,22 +110,41 @@ export class CrossLine extends Annotation {
this.middle.toggleActive(active);
}

override dragHandle(datum: CrossLineAnnotation, target: Coords, context: AnnotationContext, onInvalid: () => void) {
const { activeHandle } = this;
public dragStart(datum: CrossLineAnnotation, target: Coords, context: AnnotationContext) {
const middle =
datum.direction === 'horizontal'
? { x: target.x, y: convert(datum.value, context.yAxis) }
: { x: convert(datum.value, context.yAxis), y: target.y };

this.dragState = {
offset: target,
middle,
};
}

public drag(datum: CrossLineAnnotation, target: Coords, context: AnnotationContext, onInvalid: () => void) {
const { activeHandle, dragState } = this;

if (!activeHandle || datum.value == null) return;
let coords;

if (activeHandle) {
this[activeHandle].toggleDragging(true);
coords = this[activeHandle].drag(target).point;
} else if (dragState) {
coords = Vec2.add(dragState.middle, Vec2.sub(target, dragState.offset));
} else {
return;
}

const { direction } = datum;
this[activeHandle].toggleDragging(true);
const point = invertCoords(this[activeHandle].drag(target).point, context);
const point = invertCoords(coords, context);

if (!validateDatumPoint(context, point)) {
onInvalid();
return;
}

const horizontal = direction === 'horizontal';
datum?.set({ value: horizontal ? point.y : point.x });
const horizontal = datum.direction === 'horizontal';
datum.set({ value: horizontal ? point.y : point.x });
}

override stopDragging() {
Expand Down Expand Up @@ -169,11 +194,9 @@ export class CrossLine extends Annotation {
const scaledValue = scaleConvert(datum.value);
x1 = scaledValue;
x2 = scaledValue;
y1 = 0;
y2 = bounds.height;
} else {
const scaledValue = scaleConvert(datum.value);
x1 = 0;
x2 = bounds.width;
y1 = scaledValue;
y2 = scaledValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export class DisjointChannelAnnotation extends Annotation(
bottom.start.y -= this.startHeight;
bottom.end.y -= this.endHeight;
} else {
// TODO
_Util.Logger.warnOnce(`Annotation [${this.type}] can only be used with a numeric y-axis.`);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { AnnotationContext, Coords, LineCoords } from '../annotationTypes';
import { invertCoords } from '../annotationUtils';
import { Annotation } from '../scenes/annotation';
import { Channel } from '../scenes/channelScene';
import { ChannelScene } from '../scenes/channelScene';
import { DivariantHandle, UnivariantHandle } from '../scenes/handle';
import type { DisjointChannelAnnotation } from './disjointChannelProperties';

type ChannelHandle = keyof DisjointChannel['handles'];

export class DisjointChannel extends Channel<DisjointChannelAnnotation> {
export class DisjointChannel extends ChannelScene<DisjointChannelAnnotation> {
static override is(value: unknown): value is DisjointChannel {
return Annotation.isCheck(value, 'disjoint-channel');
}
Expand Down
Loading

0 comments on commit 88d5d60

Please sign in to comment.