diff --git a/examples/01-basic.html b/examples/01-basic.html
index 1e148f9..4583973 100644
--- a/examples/01-basic.html
+++ b/examples/01-basic.html
@@ -1,25 +1,25 @@
+
diff --git a/examples/02-thresholds.html b/examples/02-thresholds.html
index fc53645..053eadc 100644
--- a/examples/02-thresholds.html
+++ b/examples/02-thresholds.html
@@ -2,18 +2,28 @@
+
-
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 02df2f9..9d91067 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,9 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "CorpGlory",
- "license": "Apache-2.0",
+ "license": "GPL-3.0-or-later",
"dependencies": {
- "@chartwerk/core": "github:chartwerk/core#dist"
+ "@chartwerk/core": "git+ssh://git@gitlab.corpglory.com:443/chartwerk/core.git#d089b8b27c0d1137de9d7fbeddaabe3e605aba6c"
},
"devDependencies": {
"@types/d3": "^5.7.2",
diff --git a/src/gauge.ts b/src/gauge.ts
index 416eebf..3d36481 100644
--- a/src/gauge.ts
+++ b/src/gauge.ts
@@ -1,60 +1,268 @@
-import { GaugeOptions } from './types';
+import { GaugeOptions, BoundingBox } from './types';
import * as d3 from 'd3';
-export type D3SVGSelection = d3.Selection;
-export type BoundingBox = {
- x?: number, y?: number,
- width: number, height:number
-}
+export type D3SVGSelection = d3.Selection;
export class Gauge {
private _rootGroup: D3SVGSelection;
-
+ private _arcGroup: D3SVGSelection;
private _boundingBox: BoundingBox;
+ private _acrCentrum: { x: number, y: number };
+ private _value: { actual: number, original: number } // TODO: better names for this
+
private _radius: number;
- private _centrum: { x: number, y: number };
+ private _arcOuterRadius: number;
+ private _arcInnerRadius: number;
+
+ private _thresholdsVisible = false;
+ private _threasholdArcOuterRadius: number;
+ private _threasholdArcInnerRadius: number;
+ private _thresholdSteps: number[]; // steps of cutting to colors in [0..1]
+ private _valueDefined: boolean
constructor(
protected svg: D3SVGSelection,
protected readonly options: GaugeOptions
- ) {}
+ ) {
+ if(options == undefined) {
+ throw new Error("Gauge: options are not defined");
+ }
+ }
private _setBoundingBox(boundingBox: BoundingBox) {
this._boundingBox = boundingBox;
- if(this._boundingBox.x === undefined) {
- this._boundingBox.x = 0;
+ }
+
+ private _renderArcs() {
+ this._arcGroup = this._rootGroup.append('g');
+
+ let curvature = this.options.curvature;
+
+ let arcBoundingBox = {
+ width: curvature < 1 ?
+ Math.sin(curvature * Math.PI / 2) * 2: 2,
+ height: (1 - Math.cos(curvature * Math.PI / 2))
}
- if(this._boundingBox.y === undefined) {
- this._boundingBox.y = 0;
+
+ let scaleWidth = this._boundingBox.width / arcBoundingBox.width;
+ let scaleHeight = this._boundingBox.height / arcBoundingBox.height;
+
+ let minScale = Math.min(scaleWidth, scaleHeight);
+ arcBoundingBox.width *= minScale;
+ arcBoundingBox.height *= minScale;
+ let radius = minScale;
+
+ let _arcGroupX = this._boundingBox.width / 2 - arcBoundingBox.width / 2 + arcBoundingBox.width / 2;
+ let _arcGroupY = this._boundingBox.height / 2 - arcBoundingBox.height / 2 + radius;
+
+ this._arcGroup.attr(
+ "transform",
+ `translate(
+ ${_arcGroupX},
+ ${_arcGroupY}
+ )`
+ );
+
+ this._initThresholds();
+
+ this._radius = radius;
+
+ if(!this._thresholdsVisible) {
+ this._arcOuterRadius = radius;
+ this._arcInnerRadius = radius - radius * this.options.arcThickness;
+ } else {
+ this._arcOuterRadius = radius - radius * (this.options.thresholdsThickness + this.options.thresholdsOffset);
+ this._arcInnerRadius = this._arcOuterRadius - radius * this.options.arcThickness;
+
+ this._threasholdArcOuterRadius = radius;
+ this._threasholdArcInnerRadius = radius - radius * this.options.thresholdsThickness;
+ this._renderThresholds();
}
- let minWH = Math.min(this._boundingBox.width, this._boundingBox.height);
- this._radius = minWH / 2;
- this._centrum = {
- x: this._boundingBox.width / 2,
- y: this._boundingBox.height / 2,
- };
+
+ this._renderBackgroundArc();
+ this._renderValueArc();
+
}
- public render(value: number, boudingBox: BoundingBox) {
+ public render(boudingBox: BoundingBox, value?: number) {
+ // TODO: clear up value logic
+ if(value == null || value === undefined) {
+ this._valueDefined = false;
+ } else {
+ this._valueDefined = true;
+ }
+
+ this._updateValue(value);
+ this._initThresholds();
+
this._setBoundingBox(boudingBox);
this._initRootGroup();
- this._renderValueArc();
+ this._renderArcs();
+
+ this._renderLabel();
+
+ }
+
+ private _initThresholds() {
+ if (this.options.thresholds == undefined || this.options.thresholds.values.length == 0) {
+ this._thresholdsVisible = false;
+ return;
+ }
+ if (this.options.thresholds.values.length + 1 !== this.options.thresholds.colors.length) {
+ throw new Error("Colors size should be +1 of values size");
+ }
+ this._thresholdsVisible = true;
+ // TODO: throw exception if thresholds are not ordered
+
+ let steps = [0];
+ let ths = this.options.thresholds;
+ for(let i = 0; i < ths.values.length; i++) {
+ steps.push(this._getValueRanged(ths.values[i]))
+ }
+ steps.push(1);
+
+ this._thresholdSteps = steps;
+ }
+
+ private _getValueRanged(value: number) {
+ let rangeLen = this.options.range.to - this.options.range.from;
+ // we assume that this.option.stat == 'CURRENT'
+ return (value - this.options.range.from) / rangeLen;
+ }
+
+ private _updateValue(value: number) {
+ if(!this._valueDefined) {
+ return;
+ }
+ this._value = {
+ original: value,
+ actual: this._getValueRanged(value)
+ }
}
private _initRootGroup() {
this._rootGroup = this.svg.append('g');
this._rootGroup.attr(
- 'transform', `translate(${this._boundingBox.x} ${this._boundingBox.y})`
+ 'transform',
+ `translate(${this._boundingBox.x}, ${this._boundingBox.y})`
);
}
+ private _renderBackgroundArc() {
+ var arc = this._getArc(
+ 0, 1,
+ this._arcOuterRadius,
+ this._arcInnerRadius
+ )
+
+ this._arcGroup
+ .append('path')
+ .attr("d", arc)
+ .attr('fill', this.options.backgroundArcColor);
+ }
+
private _renderValueArc() {
- this._rootGroup
- .append('circle')
- .attr('cx', this._centrum.x)
- .attr('cy', this._centrum.y)
- .attr('r', this._radius)
+ if(!this._valueDefined) {
+ return;
+ }
+ var arc = this._getArc(
+ 0, this._value.actual,
+ this._arcOuterRadius,
+ this._arcInnerRadius
+ );
+
+ let color = this.options.valueArcColor;
+ if(this._thresholdsVisible) {
+ for(let i = 0; i < this._thresholdSteps.length - 1; i++) {
+ let st = this._thresholdSteps[i];
+ if(this._value.actual >= st) {
+ color = this.options.thresholds.colors[i]
+ }
+ }
+ }
+
+ this._arcGroup
+ .append('path')
+ .attr("d", arc)
+ .attr("fill", color)
+ }
+
+ private _renderLabel() {
+ // TODO: scale text to arc width
+
+ var valueText;
+ if(this.options.valueFormatter !== undefined) {
+ if(this._valueDefined) {
+ valueText = this.options.valueFormatter(this._value.original);
+ } else {
+ valueText = this.options.valueFormatter(undefined);
+ }
+ } else {
+ if(this._valueDefined) {
+ valueText = this._value.original.toString();
+ } else {
+ valueText = "no data"
+ }
+ }
+
+ // TODO: add css classes
+ let txt = this._rootGroup
+ .append("text")
+ .text(valueText)
+ .attr("dx", this._boundingBox.width / 2)
+ .attr("dy",
+ // empirical function, can considered as hack
+ this._boundingBox.height * (1 - this.options.curvature / 4)
+ )
+ .style("text-anchor", "middle");
+
+ }
+
+ private _renderThresholds() {
+ if(!this._thresholdsVisible) {
+ return;
+ }
+
+ let ths = this.options.thresholds;
+ for (let i = 0; i < this._thresholdSteps.length - 1; i++) {
+ let from = this._thresholdSteps[i];
+ let to = this._thresholdSteps[i + 1];
+
+ var arc = this._getArc(
+ from, to,
+ this._threasholdArcOuterRadius,
+ this._threasholdArcInnerRadius
+ );
+
+ this._arcGroup
+ .append('path')
+ .attr("d", arc)
+ .attr("fill", ths.colors[i])
+ }
+ }
+
+ /**
+ * Calculates arc path in respect to options.curvatur
+ * @param from beginnig of arc in persentage in 0..1
+ * @param to end of arc in persentage in o..1
+ * @returns svg path string
+ */
+ private _getArc(from: number, to: number, outerRadius: number, innerRadius:number) {
+ if(from > to) {
+ console.warn('`from` is bigger than `to`')
+ from = to;
+ }
+
+ let rFrom = this.options.curvature * (-Math.PI / 2 + Math.PI * from);
+ let rTo = this.options.curvature * (-Math.PI / 2 + Math.PI * to);
+ return d3.arc()
+ .innerRadius(innerRadius)
+ .outerRadius(outerRadius)
+ .startAngle(rFrom)
+ .endAngle(rTo)
}
+
+
}
diff --git a/src/gauge_pod.ts b/src/gauge_pod.ts
new file mode 100644
index 0000000..0ca4077
--- /dev/null
+++ b/src/gauge_pod.ts
@@ -0,0 +1,46 @@
+import { GaugeOptions, GaugeTimeSerie, GaugeOptionsUtils } from './types';
+import { Gauge } from './gauge';
+import { ChartwerkPod } from '@chartwerk/core';
+
+import * as d3 from 'd3';
+
+
+export class GaugeChartwerkPod extends ChartwerkPod {
+
+ constructor(
+ el: HTMLElement,
+ _series: GaugeTimeSerie[], // TODO: remove this
+ _options: GaugeOptions
+ ) {
+ super(
+ d3, el, _series,
+ GaugeOptionsUtils.setDefaults(_options)
+ );
+ }
+
+ renderMetrics(): void {
+ let value;
+ if (this.series.length === 0 || this.series[0].datapoints.length === 0) {
+ value = undefined;
+ } else {
+ value = this.series[this.series.length - 1].datapoints[0][0]
+ }
+ let g = new Gauge(this.chartContainer, this.options).render(
+ { x: 0, y: 0, width: this.width, height: this.height },
+ value
+ );
+ }
+
+ render() {
+ // Optimisation of rendering: we need only svg holder
+ this.renderSvg();
+ this.renderMetrics();
+ }
+
+ /* handlers and overloads */
+ onMouseOver(): void {}
+ onMouseMove(): void {}
+ onMouseOut(): void {}
+ renderSharedCrosshair(): void {}
+ hideSharedCrosshair(): void {}
+}
diff --git a/src/index.ts b/src/index.ts
index f93e198..b47b0eb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1 +1,2 @@
-export { Pod as ChartwerkGaugePod } from './pod'
+export { GaugeChartwerkPod } from './gauge_pod'
+export { ChartwerkGaugePodVue } from './pod_vue'
diff --git a/src/pod.ts b/src/pod.ts
deleted file mode 100644
index 45cc26b..0000000
--- a/src/pod.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { GaugeOptions, GaugeTimeSerie, GaugeOptionsUtils } from './types';
-import { Gauge } from './gauge';
-import { ChartwerkPod } from '@chartwerk/core';
-
-import * as d3 from 'd3';
-
-
-export class Pod extends ChartwerkPod {
-
- constructor(
- el: HTMLElement, series: GaugeTimeSerie[],
- protected readonly options: GaugeOptions
- ) {
- super(
- d3, el, series,
- GaugeOptionsUtils.setDefaults(options)
- );
- }
-
- renderMetrics(): void {
- if (this.series.length === 0 || this.series[0].datapoints.length === 0) {
- this.renderNoDataPointsMessage();
- return;
- }
- new Gauge(this.chartContainer, this.options).render(
- GaugeOptionsUtils.getValueFromDatapoints(this.options, this.series),
- { x: 0, y: 0, width: this.width, height: this.height }
- );
- }
-
- /* handlers and overloads */
- onMouseOver(): void {}
- onMouseMove(): void {}
- onMouseOut(): void {}
- renderSharedCrosshair(): void {}
- hideSharedCrosshair(): void {}
-}
diff --git a/src/pod_vue.ts b/src/pod_vue.ts
new file mode 100644
index 0000000..3518fb8
--- /dev/null
+++ b/src/pod_vue.ts
@@ -0,0 +1,30 @@
+import { VueChartwerkPodMixin } from '@chartwerk/core';
+import { GaugeChartwerkPod } from './gauge_pod'
+
+
+// it is used with Vue.component, e.g.: Vue.component('chartwerk-gauge-pod', VueChartwerkGaugePodObject)
+export const ChartwerkGaugePodVue = {
+ // alternative to `template: ''`
+ render(createElement) {
+ console.log('render in VuePod.render')
+ return createElement(
+ 'div',
+ {
+ class: { 'chartwerk-gauge-pod': true },
+ attrs: { id: this.id }
+ }
+ )
+ },
+ mixins: [VueChartwerkPodMixin],
+ methods: {
+ render() {
+ // TODO: set options properly
+ // TODO: make update insted of full rerendering
+ this.pod = new GaugeChartwerkPod(
+ document.getElementById(this.id), this.series, this.options
+ );
+ this.pod.render();
+
+ },
+ }
+};
diff --git a/src/types.ts b/src/types.ts
index f1ea351..364e1d9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,16 +1,12 @@
-import { TimeSerie, Options, ZoomType } from '@chartwerk/core';
+import { TimeSerie, Options } from '@chartwerk/core';
export type GaugeTimeSerie = TimeSerie;
-/**
- * The way to choose one value from metrics
- */
-export enum Stat {
- CURRENT = 'current',
- // MIN = 'min',
- // MAX = 'max',
- // TOTAL = 'total'
+// TODO: move to core
+export type BoundingBox = {
+ x: number, y: number,
+ width: number, height:number
}
export type Range = {
@@ -18,10 +14,6 @@ export type Range = {
to: number // should be >= from
}
-export type Threshold = {
- value: number,
- color: string
-}
// this `type` should be `class` and get all functions
// from GaugeOptionsUtils as methods;
@@ -29,9 +21,18 @@ export type Threshold = {
// all fields with "?" should be inited in constructor
// with default values, "?" should be removed after
export type GaugeOptions = Options & {
- stat: Stat,
- range?: Range
- thresholds?: Threshold[] // should be sorted and inside range
+ range?: Range,
+ thresholds?: { // colors array should be values.length + 1
+ values: number[],
+ colors: string[]
+ },
+ arcThickness?: number, // scale factor for arc innner radius
+ curvature?: number, // length of arc from 0..2 (where 2 is circle)
+ thresholdsThickness?: number
+ thresholdsOffset?: number,
+ valueArcColor?: string // used only if thresholds not defined
+ backgroundArcColor?: string // used only if thresholds not defined
+ valueFormatter?: (n?: number) => string
}
/***** OPTIONS UTILS ******/
@@ -41,13 +42,21 @@ export type GaugeOptions = Options & {
*/
export namespace GaugeOptionsUtils {
export function setChartwerkSuperPodDefaults(options: GaugeOptions): GaugeOptions {
- options.usePanning = false;
options.renderLegend = false;
- options.renderYaxis = false;
- options.renderXaxis = false;
options.renderGrid = false;
options.margin = { top: 0, right: 0, bottom: 0, left: 0 };
- options.zoom = { type: ZoomType.NONE };
+ options.axis = { x: { isActive: false }, y: { isActive: false }};
+ options.zoomEvents = {
+ mouse: {
+ zoom: { isActive: false },
+ pan: { isActive: false }
+ },
+ scroll: {
+ zoom: { isActive: false },
+ pan: { isActive: false }
+ }
+ }
+
return options;
}
@@ -56,28 +65,24 @@ export namespace GaugeOptionsUtils {
if(options.range === undefined) {
options.range = { from: 0, to: 100 };
}
- if(options.range === undefined) {
- options.thresholds = [];
+ if (options.arcThickness == undefined) {
+ options.arcThickness = 0.2;
}
- return options;
- }
-
- export function getValueFromDatapoints(
- options: GaugeOptions, series: GaugeTimeSerie[]
- ): number | null {
- // we ignore stat type and always return CURRENT stat
- if(series.length == 0) {
- throw new Error('Series are empty');
+ if (options.curvature == undefined) {
+ options.curvature = 1.5;
+ }
+ if (options.thresholdsThickness == undefined) {
+ options.thresholdsThickness = 0.1;
}
- if(series.length > 1) {
- console.warn('got to many series: ' + series.length);
+ if (options.thresholdsOffset == undefined) {
+ options.thresholdsOffset = 0.05;
}
- // we process exactly one serie
- let serie = series[0];
- if(serie.datapoints.length === 0) {
- return null;
+ if (options.valueArcColor == undefined) {
+ options.valueArcColor = 'blue';
}
- // we take value from position 1, where 0 is time
- return serie.datapoints[serie.datapoints.length - 1][1];
+ if (options.backgroundArcColor == undefined) {
+ options.backgroundArcColor = 'gray';
+ }
+ return options;
}
}