diff --git a/examples/01-basic.html b/examples/01-basic.html
index a6e1ebc..9b0e831 100644
--- a/examples/01-basic.html
+++ b/examples/01-basic.html
@@ -30,6 +30,7 @@
icons: [ { src: 'https://cityhost.ua/upload_img/blog5ef308ea5529c_trash2-01.jpg', position: 'middle', size: 30 }],
enableExtremumLabels: true,
enableThresholdLabels: true,
+ enableThresholdDrag: true,
}
);
pod.render();
diff --git a/src/index.ts b/src/index.ts
index e9cee5c..b0a1636 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-import { GaugeTimeSerie, GaugeOptions, Stat, Stop, IconConfig, IconPosition } from './types';
+import { GaugeTimeSerie, GaugeOptions, Stat, Stop, IconConfig, IconPosition, PointCoordinate } from './types';
import { ChartwerkPod, VueChartwerkPodMixin, AxisFormat, CrosshairOrientation } from '@chartwerk/core';
@@ -73,9 +73,15 @@ const DEFAULT_GAUGE_OPTIONS: GaugeOptions = {
reversed: false,
enableExtremumLabels: false,
enableThresholdLabels: false,
+ enableThresholdDrag: false,
};
export class ChartwerkGaugePod extends ChartwerkPod {
+ _draggableLines: any[] = [];
+ _draggedThresholdValues: number[] = []; // threshold values after dragging
+ _thresholdArc: any | null = null;
+ _thresholdTextLabels: any[] = [];
+
constructor(el: HTMLElement, _series: GaugeTimeSerie[] = [], _options: GaugeOptions = {}) {
super(
d3, el, _series,
@@ -91,6 +97,7 @@ export class ChartwerkGaugePod extends ChartwerkPod {
+ const spaceBetweenCircles = this.rescaleSpace(SPACE_BETWEEN_CIRCLES);
+ const thresholdInnerRadius = this._outerRadius + spaceBetweenCircles;
+ const stopCircleWidth = this.rescaleWith(STOPS_CIRCLE_WIDTH);
+ // TODO: move to options
+ const thresholdOuterRadius = thresholdInnerRadius + stopCircleWidth;
+ const arc = d3.arc()
+ .innerRadius(thresholdInnerRadius)
+ .outerRadius(thresholdOuterRadius)
+ .padAngle(0);
+ return arc;
+ }
+
+ get arcScale(): d3.ScaleLinear {
+ return this.d3.scaleLinear()
+ .domain([this.options.minValue, this.options.maxValue])
+ .range([(-1 * Math.PI) / 2 - CIRCLES_ROUNDING, Math.PI / 2 + CIRCLES_ROUNDING]);
+ }
+
+ protected _renderDraggableLines(): void {
+ if(this.options.enableThresholdDrag === false) {
+ return;
+ }
+
+ this._stopsValues.forEach((stopValue, stopIdx) => this._renderDraggableLine(stopValue, stopIdx));
+ this._draggedThresholdValues = _.clone(this._stopsValues);
+ }
+
+ protected _renderDraggableLine(stopValue: number, idx: number): void {
+ const arc = this._getThresholdArc();
+ const draggableSize = 0.025;
+ const thresholdAngle = this.arcScale(stopValue);
+
+ const pie = d3.pie()
+ .startAngle(thresholdAngle - draggableSize)
+ .endAngle(thresholdAngle + draggableSize)
+ .sort(null);
+
+ const drag = this.d3.drag()
+ .on('drag', () => this.onDrag(idx))
+ .on('end', () => this.onDragEnd(idx));
+
+ const dragLine = this.svg.selectAll(null)
+ .data(pie([1]))
+ .enter()
+ .append('path')
+ .attr('class', 'drag-line')
+ .style('fill', 'black')
+ .attr('d', arc as any)
+ .attr('transform', this._gaugeTransform)
+ .style('cursor', 'grab')
+ .attr('pointer-events', 'all');
+ dragLine.call(drag);
+ this._draggableLines.push(dragLine);
+ }
+
+ onDrag(idx: number): void {
+ const angle = this.getAngleFromCoordinates(this.d3.event.x, this.d3.event.y);
+ const restrictedAngle = this.restrictAngle(angle, idx);
+ this.updateDraggableLineByAngle(restrictedAngle, idx);
+ const value = _.ceil(this.arcScale.invert(restrictedAngle), 1);
+ this._draggedThresholdValues[idx] = value;
+ this.updateThresholdArcByNewValues(this._draggedThresholdValues);
+ this.updateThresholdLabel(value, idx);
+ if(this.options.dragCallback) {
+ this.options.dragCallback({ value, idx });
+ }
+ }
+
+ updateThresholdArcByNewValues(stops: number[]): void {
+ const thresholdArc = this._getThresholdArc();
+ const stopArcs = this._d3Pie(this._getStopsRange(stops));
+ this._thresholdArc
+ .data(stopArcs)
+ .attr('d', thresholdArc as any);
+ }
+
+ updateThresholdLabel(value: number, idx: number): void {
+ if(_.isEmpty(this._thresholdTextLabels) || !this._thresholdTextLabels[idx]) {
+ return;
+ }
+ this._thresholdTextLabels[idx].text(value);
+ }
+
+ updateDraggableLineByAngle(angle: number, idx: number): void {
+ const arc = this._getThresholdArc();
+ const draggableSize = 0.025;
+ const pie = d3.pie()
+ .startAngle(angle - draggableSize)
+ .endAngle(angle + draggableSize)
+ .sort(null);
+
+ this._draggableLines[idx].data(pie([1])).attr('d', arc);
+ }
+
+ onDragEnd(idx: number): void {
+ if(this.options.dragEndCallback) {
+ this.options.dragEndCallback({ idx });
+ }
+ }
+
+ getAngleFromCoordinates(x: number, y: number): number {
+ const vector1 = { start: this._gaugeCenterCoordinate, end: { x: this._gaugeCenterCoordinate.x, y: 0 } };
+ const vector2 = { start: this._gaugeCenterCoordinate, end: { x, y } };
+ let a1 = this.getAngleBetween2Vectors(vector1, vector2);
+ if(x < this.width / 2) { // angle < 0 degree
+ return -a1;
+ }
+ return a1;
+ }
+
+ getAngleBetween2Vectors(
+ vector1: { start: PointCoordinate, end: PointCoordinate },
+ vector2: { start: PointCoordinate, end: PointCoordinate }
+ ): number {
+ const x1 = vector1.start.x; const y1 = vector1.start.y;
+ const x2 = vector1.end.x; const y2 = vector1.end.y;
+ const x3 = vector2.start.x; const y3 = vector2.start.y;
+ const x4 = vector2.end.x; const y4 = vector2.end.y;
+ return Math.acos(
+ ((x2 - x1) * (x4 - x3) + (y2 - y1) * (y4 - y3)) /
+ (Math.sqrt( (x2 - x1)**2 + (y2 - y1)**2 ) *
+ Math.sqrt( (x4 - x3)**2 + (y4 - y3)**2 ))
+ )
+ }
+
+ restrictAngle(angle: number, idx: number): number {
+ return _.clamp(angle, -1.8, 1.8);
+ }
+
protected _renderLabels(): void {
const yOffset = this._valueTextFontSize + 8;
if(this.options.enableThresholdLabels) {
if(this._stopsValues && this._stopsValues[0]) {
this.renderLabelBackground(0, yOffset / 2);
- this.renderLabelText(this.width / 6, yOffset, String(this._stopsValues[0]));
+ this._thresholdTextLabels.push(this.renderLabelText(this.width / 6, yOffset, String(this._stopsValues[0])));
}
if(this._stopsValues && this._stopsValues[1]) {
this.renderLabelBackground(this.width * 2 / 3, yOffset / 2);
- this.renderLabelText(this.width * 5 / 6, yOffset, String(this._stopsValues[1]));
+ this._thresholdTextLabels.push(this.renderLabelText(this.width * 5 / 6, yOffset, String(this._stopsValues[1])));
}
}
if(this.options.enableExtremumLabels) {
@@ -299,8 +428,8 @@ export class ChartwerkGaugePod extends ChartwerkPod {
+ return this.svg
.append('text')
.attr('x', x)
.attr('y', y)
@@ -344,10 +473,10 @@ export class ChartwerkGaugePod extends ChartwerkPod string;
export type GaugeTimeSerie = TimeSerie;
@@ -43,5 +47,8 @@ export type GaugeOptionsParams = {
reversed: boolean;
enableThresholdLabels: boolean; // render threshold values as a text under the gauge
enableExtremumLabels: boolean; // render min/max values as a text above the gauge
+ enableThresholdDrag: boolean; // drag threshold arcs to change stops values
+ dragCallback: (event: any) => void;
+ dragEndCallback: (event: any) => void;
}
export type GaugeOptions = Options & Partial;