Browse Source

add draggable lines

merge-requests/1/merge
vargburz 3 years ago
parent
commit
b21ab5469d
  1. 1
      examples/01-basic.html
  2. 169
      src/index.ts
  3. 7
      src/types.ts

1
examples/01-basic.html

@ -30,6 +30,7 @@
icons: [ { src: 'https://cityhost.ua/upload_img/blog5ef308ea5529c_trash2-01.jpg', position: 'middle', size: 30 }], icons: [ { src: 'https://cityhost.ua/upload_img/blog5ef308ea5529c_trash2-01.jpg', position: 'middle', size: 30 }],
enableExtremumLabels: true, enableExtremumLabels: true,
enableThresholdLabels: true, enableThresholdLabels: true,
enableThresholdDrag: true,
} }
); );
pod.render(); pod.render();

169
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'; import { ChartwerkPod, VueChartwerkPodMixin, AxisFormat, CrosshairOrientation } from '@chartwerk/core';
@ -73,9 +73,15 @@ const DEFAULT_GAUGE_OPTIONS: GaugeOptions = {
reversed: false, reversed: false,
enableExtremumLabels: false, enableExtremumLabels: false,
enableThresholdLabels: false, enableThresholdLabels: false,
enableThresholdDrag: false,
}; };
export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions> { export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions> {
_draggableLines: any[] = [];
_draggedThresholdValues: number[] = []; // threshold values after dragging
_thresholdArc: any | null = null;
_thresholdTextLabels: any[] = [];
constructor(el: HTMLElement, _series: GaugeTimeSerie[] = [], _options: GaugeOptions = {}) { constructor(el: HTMLElement, _series: GaugeTimeSerie[] = [], _options: GaugeOptions = {}) {
super( super(
d3, el, _series, d3, el, _series,
@ -91,6 +97,7 @@ export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions
this._renderOverlayBackground(); this._renderOverlayBackground();
this._renderValueArc(); this._renderValueArc();
this._renderThresholdArc(); this._renderThresholdArc();
this._renderDraggableLines();
this._renderValue(); this._renderValue();
this._renderIcons(); this._renderIcons();
this._renderLabels(); this._renderLabels();
@ -110,14 +117,14 @@ export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions
} }
get _gaugeCenterTranform(): string { get _gaugeCenterTranform(): string {
return `translate(${this._gaugeCenterCoordinate.x},${0.8 * this._gaugeCenterCoordinate.y})`; return `translate(${this._gaugeCenterCoordinate.x},${this._gaugeCenterCoordinate.y})`;
} }
get _gaugeCenterCoordinate(): { x: number, y: number} { get _gaugeCenterCoordinate(): { x: number, y: number} {
// TODO: 0.8 is the hardcoded value. It can be calculated // TODO: 0.8 is the hardcoded value. It can be calculated
return { return {
x: this.width / 2 + this.margin.left, x: this.width / 2 + this.margin.left,
y: this.height y: 0.8 * this.height
} }
} }
@ -239,18 +246,10 @@ export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions
if(this._sortedStops.length === 0) { if(this._sortedStops.length === 0) {
return; return;
} }
const spaceBetweenCircles = this.rescaleSpace(SPACE_BETWEEN_CIRCLES); const thresholdArc = this._getThresholdArc();
const thresholdInnerRadius = this._outerRadius + spaceBetweenCircles;
const stopCircleWidth = this.rescaleWith(STOPS_CIRCLE_WIDTH);
// TODO: move to options
const thresholdOuterRadius = thresholdInnerRadius + stopCircleWidth;
const thresholdArc = d3.arc()
.innerRadius(thresholdInnerRadius)
.outerRadius(thresholdOuterRadius)
.padAngle(0);
const stopArcs = this._d3Pie(this._stopsRange); const stopArcs = this._d3Pie(this._getStopsRange(this._stopsValues));
this.chartContainer.selectAll(null) this._thresholdArc = this.chartContainer.selectAll(null)
.data(stopArcs) .data(stopArcs)
.enter() .enter()
.append('path') .append('path')
@ -264,16 +263,146 @@ export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions
.attr('transform', this._gaugeTransform); .attr('transform', this._gaugeTransform);
} }
protected _getThresholdArc(): d3.Arc<any, d3.DefaultArcObject> {
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<number, number> {
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 { protected _renderLabels(): void {
const yOffset = this._valueTextFontSize + 8; const yOffset = this._valueTextFontSize + 8;
if(this.options.enableThresholdLabels) { if(this.options.enableThresholdLabels) {
if(this._stopsValues && this._stopsValues[0]) { if(this._stopsValues && this._stopsValues[0]) {
this.renderLabelBackground(0, yOffset / 2); 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]) { if(this._stopsValues && this._stopsValues[1]) {
this.renderLabelBackground(this.width * 2 / 3, yOffset / 2); 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) { if(this.options.enableExtremumLabels) {
@ -299,8 +428,8 @@ export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions
.attr('fill-opacity', 0.7); .attr('fill-opacity', 0.7);
} }
protected renderLabelText(x: number, y: number, text: string): void { protected renderLabelText(x: number, y: number, text: string): d3.Selection<SVGTextElement, unknown, null, undefined> {
this.svg return this.svg
.append('text') .append('text')
.attr('x', x) .attr('x', x)
.attr('y', y) .attr('y', y)
@ -344,10 +473,10 @@ export class ChartwerkGaugePod extends ChartwerkPod<GaugeTimeSerie, GaugeOptions
} }
// TODO: better name // TODO: better name
private get _stopsRange(): number[] { private _getStopsRange(stops: number[]): number[] {
// TODO: refactor // TODO: refactor
// TODO: max value might be less than the latest stop // TODO: max value might be less than the latest stop
let stopValues = [...this._stopsValues, this._maxValue]; let stopValues = [...stops, this._maxValue];
if(stopValues.length < 2) { if(stopValues.length < 2) {
return this.getUpdatedRangeWithMinValue(stopValues); return this.getUpdatedRangeWithMinValue(stopValues);

7
src/types.ts

@ -12,6 +12,10 @@ export type Stop = {
value: number | null value: number | null
}; };
export type PointCoordinate = {
x: number, y: number
}
export type ValueFormatter = (value: number) => string; export type ValueFormatter = (value: number) => string;
export type GaugeTimeSerie = TimeSerie; export type GaugeTimeSerie = TimeSerie;
@ -43,5 +47,8 @@ export type GaugeOptionsParams = {
reversed: boolean; reversed: boolean;
enableThresholdLabels: boolean; // render threshold values as a text under the gauge enableThresholdLabels: boolean; // render threshold values as a text under the gauge
enableExtremumLabels: boolean; // render min/max values as a text above 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<GaugeOptionsParams>; export type GaugeOptions = Options & Partial<GaugeOptionsParams>;

Loading…
Cancel
Save