From 1c865fb037f55d3fc6d3b9b18126afa1a17ab410 Mon Sep 17 00:00:00 2001 From: Coin de Gamma Date: Sun, 27 Jun 2021 20:10:56 +0200 Subject: [PATCH] rendering v2 --- examples/01-basic.html | 12 +- examples/02-thresholds.html | 24 ++- examples/03-undefined-value.html | 32 ++++ package.json | 4 +- src/gauge.ts | 262 +++++++++++++++++++++++++++---- src/gauge_pod.ts | 46 ++++++ src/index.ts | 3 +- src/pod.ts | 37 ----- src/pod_vue.ts | 30 ++++ src/types.ts | 83 +++++----- 10 files changed, 414 insertions(+), 119 deletions(-) create mode 100644 examples/03-undefined-value.html create mode 100644 src/gauge_pod.ts delete mode 100644 src/pod.ts create mode 100644 src/pod_vue.ts 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; } }