From 9ce272837431c03811c45f8a2536c6fbf4c06701 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 12 Feb 2021 15:21:01 -0800 Subject: [PATCH 01/11] New computed property: uniquely Wraps up a common pattern used in charts for building a a string that incorporates the ember guid --- ui/app/utils/properties/uniquely.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 ui/app/utils/properties/uniquely.js diff --git a/ui/app/utils/properties/uniquely.js b/ui/app/utils/properties/uniquely.js new file mode 100644 index 000000000..96ce520e4 --- /dev/null +++ b/ui/app/utils/properties/uniquely.js @@ -0,0 +1,12 @@ +import { computed } from '@ember/object'; +import { guidFor } from '@ember/object/internals'; + +// An Ember.Computed property for creating a unique string with a +// common prefix (based on the guid of the object with the property) +// +// ex. @uniquely('name') // 'name-ember129383' +export default function uniquely(prefix) { + return computed(function() { + return `${prefix}-${guidFor(this)}`; + }); +} From 8c16a158f44fab6dd5bbbbcd4a6fec005eecd555 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 12 Feb 2021 15:22:17 -0800 Subject: [PATCH 02/11] Pull the Area chart primitive out of the LineChart component --- ui/app/components/chart-primitives/area.hbs | 13 +++++++ ui/app/components/chart-primitives/area.js | 37 ++++++++++++++++++ ui/app/components/line-chart.js | 43 +-------------------- ui/app/styles/charts/line-chart.scss | 22 ++++++----- ui/app/templates/components/line-chart.hbs | 25 ++++++------ 5 files changed, 74 insertions(+), 66 deletions(-) create mode 100644 ui/app/components/chart-primitives/area.hbs create mode 100644 ui/app/components/chart-primitives/area.js diff --git a/ui/app/components/chart-primitives/area.hbs b/ui/app/components/chart-primitives/area.hbs new file mode 100644 index 000000000..4ba8af25f --- /dev/null +++ b/ui/app/components/chart-primitives/area.hbs @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ui/app/components/chart-primitives/area.js b/ui/app/components/chart-primitives/area.js new file mode 100644 index 000000000..5c5179f33 --- /dev/null +++ b/ui/app/components/chart-primitives/area.js @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; +import { default as d3Shape, area, line } from 'd3-shape'; +import uniquely from 'nomad-ui/utils/properties/uniquely'; + +export default class ChartPrimitiveArea extends Component { + get colorClass() { + return this.args.colorClass || `${this.args.colorScale}-${this.args.index}`; + } + + @uniquely('area-mask') maskId; + @uniquely('area-fill') fillId; + + get line() { + const { xScale, yScale, xProp, yProp, curveMethod } = this.args; + + const builder = line() + .curve(d3Shape[curveMethod]) + .defined(d => d[yProp] != null) + .x(d => xScale(d[xProp])) + .y(d => yScale(d[yProp])); + + return builder(this.args.data); + } + + get area() { + const { xScale, yScale, xProp, yProp, curveMethod } = this.args; + + const builder = area() + .curve(d3Shape[curveMethod]) + .defined(d => d[yProp] != null) + .x(d => xScale(d[xProp])) + .y0(yScale(0)) + .y1(d => yScale(d[yProp])); + + return builder(this.args.data); + } +} diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index 2e1561c83..ed80edd88 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -4,14 +4,12 @@ import { computed } from '@ember/object'; import { assert } from '@ember/debug'; import { observes } from '@ember-decorators/object'; import { computed as overridable } from 'ember-overridable-computed'; -import { guidFor } from '@ember/object/internals'; import { run } from '@ember/runloop'; import { htmlSafe } from '@ember/template'; import d3 from 'd3-selection'; import d3Scale from 'd3-scale'; import d3Axis from 'd3-axis'; import d3Array from 'd3-array'; -import d3Shape from 'd3-shape'; import d3Format from 'd3-format'; import d3TimeFormat from 'd3-time-format'; import WindowResizable from 'nomad-ui/mixins/window-resizable'; @@ -73,16 +71,6 @@ export default class LineChart extends Component.extend(WindowResizable) { isActive = false; - @computed() - get fillId() { - return `line-chart-fill-${guidFor(this)}`; - } - - @computed() - get maskId() { - return `line-chart-mask-${guidFor(this)}`; - } - activeDatum = null; @computed('activeDatum', 'timeseries', 'xProp') @@ -252,35 +240,6 @@ export default class LineChart extends Component.extend(WindowResizable) { return this.width - this.yAxisWidth; } - @computed('data.[]', 'xScale', 'yScale', 'curveMethod') - get line() { - const { xScale, yScale, xProp, yProp, curveMethod } = this; - - const line = d3Shape - .line() - .curve(d3Shape[curveMethod]) - .defined(d => d[yProp] != null) - .x(d => xScale(d[xProp])) - .y(d => yScale(d[yProp])); - - return line(this.data); - } - - @computed('data.[]', 'xScale', 'yScale', 'curveMethod') - get area() { - const { xScale, yScale, xProp, yProp, curveMethod } = this; - - const area = d3Shape - .area() - .curve(d3Shape[curveMethod]) - .defined(d => d[yProp] != null) - .x(d => xScale(d[xProp])) - .y0(yScale(0)) - .y1(d => yScale(d[yProp])); - - return area(this.data); - } - @computed('annotations.[]', 'xScale', 'xProp', 'timeseries') get processedAnnotations() { const { xScale, xProp, annotations, timeseries } = this; @@ -319,7 +278,7 @@ export default class LineChart extends Component.extend(WindowResizable) { didInsertElement() { this.updateDimensions(); - const canvas = d3.select(this.element.querySelector('.canvas')); + const canvas = d3.select(this.element.querySelector('.hover-target')); const updateActiveDatum = this.updateActiveDatum.bind(this); const chart = this; diff --git a/ui/app/styles/charts/line-chart.scss b/ui/app/styles/charts/line-chart.scss index 3b85e9262..3f447c58a 100644 --- a/ui/app/styles/charts/line-chart.scss +++ b/ui/app/styles/charts/line-chart.scss @@ -14,16 +14,18 @@ overflow: visible; } - .canvas { - .line { - fill: transparent; - stroke-width: 1.25; - } + .hover-target { + fill: transparent; + stroke: transparent; + } - .hover-target { - fill: transparent; - stroke: transparent; - } + .line { + fill: transparent; + stroke-width: 1.25; + } + + .area { + fill: none; } .axis { @@ -60,7 +62,7 @@ @each $name, $pair in $colors { $color: nth($pair, 1); - .canvas.is-#{$name} { + .area.is-#{$name} { .line { stroke: $color; } diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index 8c945fca5..521dd2c47 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -8,23 +8,20 @@ and Y-axis values range from {{this.yRange.firstObject}} to {{this.yRange.lastObject}}. {{/if}} - - - - - - - - - - - - - - + +
{{#each this.processedAnnotations key=this.annotationKey as |annotation|}} From ba22533d968bc8ae9fe1697b7bebd46b5bd8ccdd Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 12 Feb 2021 17:50:18 -0800 Subject: [PATCH 03/11] ArrayProxy evidently isn't iterable --- ui/stories/utils/delayed-array.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/stories/utils/delayed-array.js b/ui/stories/utils/delayed-array.js index f09618538..202e21dc2 100644 --- a/ui/stories/utils/delayed-array.js +++ b/ui/stories/utils/delayed-array.js @@ -12,9 +12,11 @@ export default ArrayProxy.extend({ init(array) { this.set('content', A([])); this._super(...arguments); + this[Symbol.iterator] = this.content[Symbol.iterator]; next(this, () => { this.set('content', A(array)); + this[Symbol.iterator] = this.content[Symbol.iterator]; }); }, }); From 1f14fd5ebf0845a7ce700607d9fc745292e6ee41 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 12 Feb 2021 19:41:14 -0800 Subject: [PATCH 04/11] Pull the VAnnotations primitive out of the LineChart component --- .../chart-primitives/v-annotations.hbs | 17 ++++ .../chart-primitives/v-annotations.js | 85 +++++++++++++++++++ ui/app/components/line-chart.js | 50 +---------- ui/app/templates/components/line-chart.hbs | 26 ++---- ui/stories/charts/line-chart.stories.js | 2 + 5 files changed, 114 insertions(+), 66 deletions(-) create mode 100644 ui/app/components/chart-primitives/v-annotations.hbs create mode 100644 ui/app/components/chart-primitives/v-annotations.js diff --git a/ui/app/components/chart-primitives/v-annotations.hbs b/ui/app/components/chart-primitives/v-annotations.hbs new file mode 100644 index 000000000..0c61c82b0 --- /dev/null +++ b/ui/app/components/chart-primitives/v-annotations.hbs @@ -0,0 +1,17 @@ +
+ {{#each this.processed key=@key as |annotation|}} +
+ +
+
+ {{/each}} +
diff --git a/ui/app/components/chart-primitives/v-annotations.js b/ui/app/components/chart-primitives/v-annotations.js new file mode 100644 index 000000000..0b64141f1 --- /dev/null +++ b/ui/app/components/chart-primitives/v-annotations.js @@ -0,0 +1,85 @@ +import Component from '@glimmer/component'; +import { htmlSafe } from '@ember/template'; +import { action } from '@ember/object'; + +const iconFor = { + error: 'cancel-circle-fill', + info: 'info-circle-fill', +}; + +const iconClassFor = { + error: 'is-danger', + info: '', +}; + +// TODO: This is what styleStringProperty looks like in the pure decorator world +function styleString(target, name, descriptor) { + if (!descriptor.get) throw new Error('styleString only works on getters'); + const orig = descriptor.get; + descriptor.get = function() { + const styles = orig.apply(this); + + let str = ''; + + if (styles) { + str = Object.keys(styles) + .reduce(function(arr, key) { + const val = styles[key]; + arr.push(key + ':' + (typeof val === 'number' ? val.toFixed(2) + 'px' : val)); + return arr; + }, []) + .join(';'); + } + + return htmlSafe(str); + }; + return descriptor; +} + +export default class ChartPrimitiveVAnnotations extends Component { + @styleString + get chartAnnotationsStyle() { + return { + height: this.args.height, + }; + } + + get processed() { + const { scale, prop, annotations, timeseries, format } = this.args; + + if (!annotations || !annotations.length) return null; + + let sortedAnnotations = annotations.sortBy(prop); + if (timeseries) { + sortedAnnotations = sortedAnnotations.reverse(); + } + + let prevX = 0; + let prevHigh = false; + return sortedAnnotations.map(annotation => { + const x = scale(annotation[prop]); + if (prevX && !prevHigh && Math.abs(x - prevX) < 30) { + prevHigh = true; + } else if (prevHigh) { + prevHigh = false; + } + const y = prevHigh ? -15 : 0; + const formattedX = format(timeseries)(annotation[prop]); + + prevX = x; + return { + annotation, + style: htmlSafe(`transform:translate(${x}px,${y}px)`), + icon: iconFor[annotation.type], + iconClass: iconClassFor[annotation.type], + staggerClass: prevHigh ? 'is-staggered' : '', + label: `${annotation.type} event at ${formattedX}`, + }; + }); + } + + @action + selectAnnotation(annotation) { + if (this.args.annotationClick) this.args.annotationClick(annotation); + } +} diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index ed80edd88..c4bf2074d 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -5,7 +5,6 @@ import { assert } from '@ember/debug'; import { observes } from '@ember-decorators/object'; import { computed as overridable } from 'ember-overridable-computed'; import { run } from '@ember/runloop'; -import { htmlSafe } from '@ember/template'; import d3 from 'd3-selection'; import d3Scale from 'd3-scale'; import d3Axis from 'd3-axis'; @@ -14,7 +13,7 @@ import d3Format from 'd3-format'; import d3TimeFormat from 'd3-time-format'; import WindowResizable from 'nomad-ui/mixins/window-resizable'; import styleStringProperty from 'nomad-ui/utils/properties/style-string'; -import { classNames, classNameBindings } from '@ember-decorators/component'; +import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; // Returns a new array with the specified number of points linearly @@ -31,24 +30,12 @@ const lerp = ([low, high], numPoints) => { // Round a number or an array of numbers const nice = val => (val instanceof Array ? val.map(nice) : Math.round(val)); -const iconFor = { - error: 'cancel-circle-fill', - info: 'info-circle-fill', -}; - -const iconClassFor = { - error: 'is-danger', - info: '', -}; - @classic @classNames('chart', 'line-chart') -@classNameBindings('annotations.length:with-annotations') export default class LineChart extends Component.extend(WindowResizable) { // Public API data = null; - annotations = null; activeAnnotation = null; onAnnotationClick() {} xProp = null; @@ -240,41 +227,6 @@ export default class LineChart extends Component.extend(WindowResizable) { return this.width - this.yAxisWidth; } - @computed('annotations.[]', 'xScale', 'xProp', 'timeseries') - get processedAnnotations() { - const { xScale, xProp, annotations, timeseries } = this; - - if (!annotations || !annotations.length) return null; - - let sortedAnnotations = annotations.sortBy(xProp); - if (timeseries) { - sortedAnnotations = sortedAnnotations.reverse(); - } - - let prevX = 0; - let prevHigh = false; - return sortedAnnotations.map(annotation => { - const x = xScale(annotation[xProp]); - if (prevX && !prevHigh && Math.abs(x - prevX) < 30) { - prevHigh = true; - } else if (prevHigh) { - prevHigh = false; - } - const y = prevHigh ? -15 : 0; - const formattedX = this.xFormat(timeseries)(annotation[xProp]); - - prevX = x; - return { - annotation, - style: htmlSafe(`transform:translate(${x}px,${y}px)`), - icon: iconFor[annotation.type], - iconClass: iconClassFor[annotation.type], - staggerClass: prevHigh ? 'is-staggered' : '', - label: `${annotation.type} event at ${formattedX}`, - }; - }); - } - didInsertElement() { this.updateDimensions(); diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index 521dd2c47..317b22b43 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -23,23 +23,15 @@ -
- {{#each this.processedAnnotations key=this.annotationKey as |annotation|}} -
- -
-
- {{/each}} -
+

diff --git a/ui/stories/charts/line-chart.stories.js b/ui/stories/charts/line-chart.stories.js index ddd9debff..e3a1b1fb3 100644 --- a/ui/stories/charts/line-chart.stories.js +++ b/ui/stories/charts/line-chart.stories.js @@ -153,6 +153,7 @@ export let Annotations = () => {

{{#if (and this.data this.annotations)}} {
{{#if (and this.data this.annotations)}} Date: Mon, 22 Feb 2021 12:59:11 -0800 Subject: [PATCH 05/11] Move new glimmer style string to its own home --- .../chart-primitives/v-annotations.js | 25 +-------------- .../utils/properties/glimmer-style-string.js | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 ui/app/utils/properties/glimmer-style-string.js diff --git a/ui/app/components/chart-primitives/v-annotations.js b/ui/app/components/chart-primitives/v-annotations.js index 0b64141f1..f05481778 100644 --- a/ui/app/components/chart-primitives/v-annotations.js +++ b/ui/app/components/chart-primitives/v-annotations.js @@ -1,6 +1,7 @@ import Component from '@glimmer/component'; import { htmlSafe } from '@ember/template'; import { action } from '@ember/object'; +import styleString from 'nomad-ui/utils/properties/glimmer-style-string'; const iconFor = { error: 'cancel-circle-fill', @@ -12,30 +13,6 @@ const iconClassFor = { info: '', }; -// TODO: This is what styleStringProperty looks like in the pure decorator world -function styleString(target, name, descriptor) { - if (!descriptor.get) throw new Error('styleString only works on getters'); - const orig = descriptor.get; - descriptor.get = function() { - const styles = orig.apply(this); - - let str = ''; - - if (styles) { - str = Object.keys(styles) - .reduce(function(arr, key) { - const val = styles[key]; - arr.push(key + ':' + (typeof val === 'number' ? val.toFixed(2) + 'px' : val)); - return arr; - }, []) - .join(';'); - } - - return htmlSafe(str); - }; - return descriptor; -} - export default class ChartPrimitiveVAnnotations extends Component { @styleString get chartAnnotationsStyle() { diff --git a/ui/app/utils/properties/glimmer-style-string.js b/ui/app/utils/properties/glimmer-style-string.js new file mode 100644 index 000000000..cde60808f --- /dev/null +++ b/ui/app/utils/properties/glimmer-style-string.js @@ -0,0 +1,31 @@ +import { htmlSafe } from '@ember/template'; + +// A decorator for transforming an object into an html +// compatible style attribute string. +// +// ex. @styleString +// get styleStr() { +// return { color: '#FF0', 'border-width': '1px' } +// } +export default function styleString(target, name, descriptor) { + if (!descriptor.get) throw new Error('styleString only works on getters'); + const orig = descriptor.get; + descriptor.get = function() { + const styles = orig.apply(this); + + let str = ''; + + if (styles) { + str = Object.keys(styles) + .reduce(function(arr, key) { + const val = styles[key]; + arr.push(key + ':' + (typeof val === 'number' ? val.toFixed(2) + 'px' : val)); + return arr; + }, []) + .join(';'); + } + + return htmlSafe(str); + }; + return descriptor; +} From 1e18be17578246c4f7b92e7ef7eb7df1eaecff9e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Feb 2021 13:26:05 -0800 Subject: [PATCH 06/11] Convert LineChart into a glimmer component --- ui/app/components/line-chart.js | 211 +++++++++------------ ui/app/templates/components/line-chart.hbs | 91 +++++---- 2 files changed, 137 insertions(+), 165 deletions(-) diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index c4bf2074d..8a97b3bd1 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -1,9 +1,8 @@ /* eslint-disable ember/no-observers */ -import Component from '@ember/component'; -import { computed } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; import { assert } from '@ember/debug'; -import { observes } from '@ember-decorators/object'; -import { computed as overridable } from 'ember-overridable-computed'; import { run } from '@ember/runloop'; import d3 from 'd3-selection'; import d3Scale from 'd3-scale'; @@ -11,10 +10,7 @@ import d3Axis from 'd3-axis'; import d3Array from 'd3-array'; import d3Format from 'd3-format'; import d3TimeFormat from 'd3-time-format'; -import WindowResizable from 'nomad-ui/mixins/window-resizable'; -import styleStringProperty from 'nomad-ui/utils/properties/style-string'; -import { classNames } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; +import styleString from 'nomad-ui/utils/properties/glimmer-style-string'; // Returns a new array with the specified number of points linearly // distributed across the bounds @@ -30,64 +26,41 @@ const lerp = ([low, high], numPoints) => { // Round a number or an array of numbers const nice = val => (val instanceof Array ? val.map(nice) : Math.round(val)); -@classic -@classNames('chart', 'line-chart') -export default class LineChart extends Component.extend(WindowResizable) { - // Public API +export default class LineChart extends Component { + /** Args + data = null; + xProp = null; + yProp = null; + curve = 'linear'; + title = 'Line Chart'; + description = null; + timeseries = false; + chartClass = 'is-primary'; + activeAnnotation = null; + onAnnotationClick() {} + */ - data = null; - activeAnnotation = null; - onAnnotationClick() {} - xProp = null; - yProp = null; - curve = 'linear'; - timeseries = false; - chartClass = 'is-primary'; + @tracked width = 0; + @tracked height = 0; + @tracked isActive = false; + @tracked activeDatum = null; + @tracked tooltipPosition = null; + @tracked element = null; - title = 'Line Chart'; - - @overridable(function() { - return null; - }) - description; - - // Private Properties - - width = 0; - height = 0; - - isActive = false; - - activeDatum = null; - - @computed('activeDatum', 'timeseries', 'xProp') - get activeDatumLabel() { - const datum = this.activeDatum; - - if (!datum) return undefined; - - const x = datum[this.xProp]; - return this.xFormat(this.timeseries)(x); + get xProp() { + return this.args.xProp || 'time'; } - - @computed('activeDatum', 'yProp') - get activeDatumValue() { - const datum = this.activeDatum; - - if (!datum) return undefined; - - const y = datum[this.yProp]; - return this.yFormat()(y); + get yProp() { + return this.args.yProp || 'value'; } - - @computed('curve') - get curveMethod() { - const mappings = { - linear: 'curveLinear', - stepAfter: 'curveStepAfter', - }; - assert(`Provided curve "${this.curve}" is not an allowed curve type`, mappings[this.curve]); - return mappings[this.curve]; + get data() { + return this.args.data || []; + } + get curve() { + return this.args.curve || 'linear'; + } + get chartClass() { + return this.args.chartClass || 'is-primary'; } // Overridable functions that retrurn formatter functions @@ -99,40 +72,57 @@ export default class LineChart extends Component.extend(WindowResizable) { return d3Format.format(',.2~r'); } - tooltipPosition = null; - @styleStringProperty('tooltipPosition') tooltipStyle; + get activeDatumLabel() { + const datum = this.activeDatum; - @computed('xAxisOffset') - get chartAnnotationBounds() { - return { - height: this.xAxisOffset, - }; + if (!datum) return undefined; + + const x = datum[this.xProp]; + return this.xFormat(this.args.timeseries)(x); + } + + get activeDatumValue() { + const datum = this.activeDatum; + + if (!datum) return undefined; + + const y = datum[this.yProp]; + return this.yFormat()(y); + } + + get curveMethod() { + const mappings = { + linear: 'curveLinear', + stepAfter: 'curveStepAfter', + }; + assert(`Provided curve "${this.curve}" is not an allowed curve type`, mappings[this.curve]); + return mappings[this.curve]; + } + + @styleString + get tooltipStyle() { + return this.tooltipPosition; } - @styleStringProperty('chartAnnotationBounds') chartAnnotationsStyle; - @computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset') get xScale() { - const xProp = this.xProp; - const scale = this.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); - const data = this.data; + const { xProp, data } = this; + const scale = this.args.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); - const domain = data.length ? d3Array.extent(this.data, d => d[xProp]) : [0, 1]; + const domain = data.length ? d3Array.extent(data, d => d[xProp]) : [0, 1]; scale.rangeRound([10, this.yAxisOffset]).domain(domain); return scale; } - @computed('data.[]', 'xFormat', 'xProp', 'timeseries') get xRange() { - const { xProp, timeseries, data } = this; + const { xProp, data } = this; const range = d3Array.extent(data, d => d[xProp]); - const formatter = this.xFormat(timeseries); + const formatter = this.xFormat(this.args.timeseries); return range.map(formatter); } - @computed('data.[]', 'yFormat', 'yProp') get yRange() { const yProp = this.yProp; const range = d3Array.extent(this.data, d => d[yProp]); @@ -141,7 +131,6 @@ export default class LineChart extends Component.extend(WindowResizable) { return range.map(formatter); } - @computed('data.[]', 'yProp', 'xAxisOffset') get yScale() { const yProp = this.yProp; let max = d3Array.max(this.data, d => d[yProp]) || 1; @@ -155,9 +144,8 @@ export default class LineChart extends Component.extend(WindowResizable) { .domain([0, max]); } - @computed('timeseries', 'xScale') get xAxis() { - const formatter = this.xFormat(this.timeseries); + const formatter = this.xFormat(this.args.timeseries); return d3Axis .axisBottom() @@ -166,7 +154,6 @@ export default class LineChart extends Component.extend(WindowResizable) { .tickFormat(formatter); } - @computed('xAxisOffset', 'yScale') get yTicks() { const height = this.xAxisOffset; const tickCount = Math.ceil(height / 120) * 2 + 1; @@ -175,7 +162,6 @@ export default class LineChart extends Component.extend(WindowResizable) { return domain[1] - domain[0] > 1 ? nice(ticks) : ticks; } - @computed('yScale', 'yTicks') get yAxis() { const formatter = this.yFormat(); @@ -186,7 +172,6 @@ export default class LineChart extends Component.extend(WindowResizable) { .tickFormat(formatter); } - @computed('yAxisOffset', 'yScale', 'yTicks') get yGridlines() { // The first gridline overlaps the x-axis, so remove it const [, ...ticks] = this.yTicks; @@ -199,7 +184,6 @@ export default class LineChart extends Component.extend(WindowResizable) { .tickFormat(''); } - @computed('element') get xAxisHeight() { // Avoid divide by zero errors by always having a height if (!this.element) return 1; @@ -208,7 +192,6 @@ export default class LineChart extends Component.extend(WindowResizable) { return axis && axis.getBBox().height; } - @computed('element') get yAxisWidth() { // Avoid divide by zero errors by always having a width if (!this.element) return 1; @@ -217,17 +200,17 @@ export default class LineChart extends Component.extend(WindowResizable) { return axis && axis.getBBox().width; } - @overridable('height', 'xAxisHeight', function() { + get xAxisOffset() { return this.height - this.xAxisHeight; - }) - xAxisOffset; + } - @computed('width', 'yAxisWidth') get yAxisOffset() { return this.width - this.yAxisWidth; } - didInsertElement() { + @action + onInsert(element) { + this.element = element; this.updateDimensions(); const canvas = d3.select(this.element.querySelector('.hover-target')); @@ -236,27 +219,23 @@ export default class LineChart extends Component.extend(WindowResizable) { const chart = this; canvas.on('mouseenter', function() { const mouseX = d3.mouse(this)[0]; - chart.set('latestMouseX', mouseX); + chart.latestMouseX = mouseX; updateActiveDatum(mouseX); - run.schedule('afterRender', chart, () => chart.set('isActive', true)); + run.schedule('afterRender', chart, () => (chart.isActive = true)); }); canvas.on('mousemove', function() { const mouseX = d3.mouse(this)[0]; - chart.set('latestMouseX', mouseX); + chart.latestMouseX = mouseX; updateActiveDatum(mouseX); }); canvas.on('mouseleave', () => { - run.schedule('afterRender', this, () => this.set('isActive', false)); - this.set('activeDatum', null); + run.schedule('afterRender', this, () => (this.isActive = false)); + this.activeDatum = null; }); } - didUpdateAttrs() { - this.renderChart(); - } - updateActiveDatum(mouseX) { const { xScale, xProp, yScale, yProp, data } = this; @@ -281,16 +260,11 @@ export default class LineChart extends Component.extend(WindowResizable) { datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft; } - this.set('activeDatum', datum); - this.set('tooltipPosition', { + this.activeDatum = datum; + this.tooltipPosition = { left: xScale(datum[xProp]), top: yScale(datum[yProp]) - 10, - }); - } - - @observes('data.[]') - updateChart() { - this.renderChart(); + }; } // The renderChart method should only ever be responsible for runtime calculations @@ -299,16 +273,11 @@ export default class LineChart extends Component.extend(WindowResizable) { // There is nothing to do if the element hasn't been inserted yet if (!this.element) return; - // First, create the axes to get the dimensions of the resulting + // Create the axes to get the dimensions of the resulting // svg elements this.mountD3Elements(); run.next(() => { - // Then, recompute anything that depends on the dimensions - // on the dimensions of the axes elements - this.notifyPropertyChange('xAxisHeight'); - this.notifyPropertyChange('yAxisWidth'); - // Since each axis depends on the dimension of the other // axis, the axes themselves are recomputed and need to // be re-rendered. @@ -328,19 +297,15 @@ export default class LineChart extends Component.extend(WindowResizable) { } annotationClick(annotation) { - this.onAnnotationClick(annotation); - } - - windowResizeHandler() { - run.once(this, this.updateDimensions); + this.args.onAnnotationClick && this.args.onAnnotationClick(annotation); } + @action updateDimensions() { const $svg = this.element.querySelector('svg'); - const width = $svg.clientWidth; - const height = $svg.clientHeight; - this.setProperties({ width, height }); + this.height = $svg.clientHeight; + this.width = $svg.clientWidth; this.renderChart(); } } diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index 317b22b43..b4f9c7c41 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -1,43 +1,50 @@ - - {{this.title}} - - {{#if this.description}} - {{this.description}} - {{else}} - X-axis values range from {{this.xRange.firstObject}} to {{this.xRange.lastObject}}, - and Y-axis values range from {{this.yRange.firstObject}} to {{this.yRange.lastObject}}. - {{/if}} - - - - - - - - -
-

- - - {{this.activeDatumLabel}} - - {{this.activeDatumValue}} -

+
+ + {{this.title}} + + {{#if this.description}} + {{this.description}} + {{else}} + X-axis values range from {{this.xRange.firstObject}} to {{this.xRange.lastObject}}, + and Y-axis values range from {{this.yRange.firstObject}} to {{this.yRange.lastObject}}. + {{/if}} + + + + + + + + +
+

+ + + {{this.activeDatumLabel}} + + {{this.activeDatumValue}} +

+
From ae381d1a20c46717e61bffc4484bc4ddbc1c53cf Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Feb 2021 17:33:10 -0800 Subject: [PATCH 07/11] Defensive arguments for glimmer-factory --- ui/tests/helpers/glimmer-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/helpers/glimmer-factory.js b/ui/tests/helpers/glimmer-factory.js index c9cd865d3..5525eee7b 100644 --- a/ui/tests/helpers/glimmer-factory.js +++ b/ui/tests/helpers/glimmer-factory.js @@ -24,7 +24,7 @@ export default function setupGlimmerComponentFactory(hooks, componentKey) { // Look up the component class in the glimmer component manager and return a // function to construct components as if they were functions. function glimmerComponentInstantiator(owner, componentKey) { - return args => { + return (args = {}) => { const componentManager = owner.lookup('component-manager:glimmer'); const componentClass = owner.factoryFor(`component:${componentKey}`).class; return componentManager.createComponent(componentClass, { named: args }); From 12fb78c3ab5cee2d2bd060577641bf331148bf43 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Feb 2021 17:34:24 -0800 Subject: [PATCH 08/11] Refactor line chart scales and refactor tests --- ui/app/components/line-chart.js | 53 +++++++----- ui/app/templates/components/line-chart.hbs | 10 +-- .../integration/components/line-chart-test.js | 34 ++++++-- ui/tests/unit/components/line-chart-test.js | 80 ++++++++----------- 4 files changed, 100 insertions(+), 77 deletions(-) diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index 8a97b3bd1..12323685e 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -1,4 +1,3 @@ -/* eslint-disable ember/no-observers */ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; @@ -11,6 +10,7 @@ import d3Array from 'd3-array'; import d3Format from 'd3-format'; import d3TimeFormat from 'd3-time-format'; import styleString from 'nomad-ui/utils/properties/glimmer-style-string'; +import uniquely from 'nomad-ui/utils/properties/uniquely'; // Returns a new array with the specified number of points linearly // distributed across the bounds @@ -26,6 +26,27 @@ const lerp = ([low, high], numPoints) => { // Round a number or an array of numbers const nice = val => (val instanceof Array ? val.map(nice) : Math.round(val)); +const defaultXScale = (data, yAxisOffset, xProp, timeseries) => { + const scale = timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); + const domain = data.length ? d3Array.extent(data, d => d[xProp]) : [0, 1]; + + scale.rangeRound([10, yAxisOffset]).domain(domain); + + return scale; +}; + +const defaultYScale = (data, xAxisOffset, yProp) => { + let max = d3Array.max(data, d => d[yProp]) || 1; + if (max > 1) { + max = nice(max); + } + + return d3Scale + .scaleLinear() + .rangeRound([xAxisOffset, 10]) + .domain([0, max]); +}; + export default class LineChart extends Component { /** Args data = null; @@ -47,6 +68,9 @@ export default class LineChart extends Component { @tracked tooltipPosition = null; @tracked element = null; + @uniquely('title') titleId; + @uniquely('desc') descriptionId; + get xProp() { return this.args.xProp || 'time'; } @@ -63,12 +87,15 @@ export default class LineChart extends Component { return this.args.chartClass || 'is-primary'; } - // Overridable functions that retrurn formatter functions + @action xFormat(timeseries) { + if (this.args.xFormat) return this.args.xFormat; return timeseries ? d3TimeFormat.timeFormat('%b %d, %H:%M') : d3Format.format(','); } + @action yFormat() { + if (this.args.yFormat) return this.args.yFormat; return d3Format.format(',.2~r'); } @@ -105,14 +132,8 @@ export default class LineChart extends Component { } get xScale() { - const { xProp, data } = this; - const scale = this.args.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); - - const domain = data.length ? d3Array.extent(data, d => d[xProp]) : [0, 1]; - - scale.rangeRound([10, this.yAxisOffset]).domain(domain); - - return scale; + const fn = this.args.xScale || defaultXScale; + return fn(this.data, this.yAxisOffset, this.xProp, this.args.timeseries); } get xRange() { @@ -132,16 +153,8 @@ export default class LineChart extends Component { } get yScale() { - const yProp = this.yProp; - let max = d3Array.max(this.data, d => d[yProp]) || 1; - if (max > 1) { - max = nice(max); - } - - return d3Scale - .scaleLinear() - .rangeRound([this.xAxisOffset, 10]) - .domain([0, max]); + const fn = this.args.yScale || defaultYScale; + return fn(this.data, this.xAxisOffset, this.yProp); } get xAxis() { diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index b4f9c7c41..f8d03f41d 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -4,16 +4,16 @@ {{did-insert this.onInsert}} {{did-update this.renderChart}} {{window-resize this.updateDimensions}}> - - {{this.title}} - + + {{this.title}} + {{#if this.description}} {{this.description}} {{else}} X-axis values range from {{this.xRange.firstObject}} to {{this.xRange.lastObject}}, and Y-axis values range from {{this.yRange.firstObject}} to {{this.yRange.lastObject}}. {{/if}} - + Date: Mon, 22 Feb 2021 17:36:50 -0800 Subject: [PATCH 09/11] Refactor StatsTimeSeries component to be a glimmer component and use composition instead of inheritance --- ui/app/components/stats-time-series.js | 53 ++++++------------- .../components/stats-time-series.hbs | 12 +++++ .../components/primary-metric-test.js | 4 +- .../unit/components/stats-time-series-test.js | 52 +++++++----------- 4 files changed, 51 insertions(+), 70 deletions(-) create mode 100644 ui/app/templates/components/stats-time-series.hbs diff --git a/ui/app/components/stats-time-series.js b/ui/app/components/stats-time-series.js index 58b9643d5..d8363ed8f 100644 --- a/ui/app/components/stats-time-series.js +++ b/ui/app/components/stats-time-series.js @@ -1,39 +1,26 @@ -import { computed } from '@ember/object'; +import Component from '@glimmer/component'; import moment from 'moment'; import d3TimeFormat from 'd3-time-format'; import d3Format from 'd3-format'; -import d3Scale from 'd3-scale'; +import { scaleTime, scaleLinear } from 'd3-scale'; import d3Array from 'd3-array'; -import LineChart from 'nomad-ui/components/line-chart'; -import layout from '../templates/components/line-chart'; import formatDuration from 'nomad-ui/utils/format-duration'; -import classic from 'ember-classic-decorator'; -@classic -export default class StatsTimeSeries extends LineChart { - layout = layout; - - xProp = 'timestamp'; - yProp = 'percent'; - timeseries = true; - - xFormat() { +export default class StatsTimeSeries extends Component { + get xFormat() { return d3TimeFormat.timeFormat('%H:%M:%S'); } - yFormat() { + get yFormat() { return d3Format.format('.1~%'); } // Specific a11y descriptors - title = 'Stats Time Series Chart'; - - @computed('data.[]', 'xProp', 'yProp') get description() { - const { xProp, yProp, data } = this; - const yRange = d3Array.extent(data, d => d[yProp]); - const xRange = d3Array.extent(data, d => d[xProp]); - const yFormatter = this.yFormat(); + const data = this.args.data; + const yRange = d3Array.extent(data, d => d.percent); + const xRange = d3Array.extent(data, d => d.timestamp); + const yFormatter = this.yFormat; const duration = formatDuration(xRange[1] - xRange[0], 'ms', true); @@ -42,36 +29,30 @@ export default class StatsTimeSeries extends LineChart { )} to ${yFormatter(yRange[1])}`; } - @computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset') - get xScale() { - const xProp = this.xProp; - const scale = this.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear(); - const data = this.data; + xScale(data, yAxisOffset) { + const scale = scaleTime(); - const [low, high] = d3Array.extent(data, d => d[xProp]); + const [low, high] = d3Array.extent(data, d => d.timestamp); const minLow = moment(high) .subtract(5, 'minutes') .toDate(); const extent = data.length ? [Math.min(low, minLow), high] : [minLow, new Date()]; - scale.rangeRound([10, this.yAxisOffset]).domain(extent); + scale.rangeRound([10, yAxisOffset]).domain(extent); return scale; } - @computed('data.[]', 'yProp', 'xAxisOffset') - get yScale() { - const yProp = this.yProp; - const yValues = (this.data || []).mapBy(yProp); + yScale(data, xAxisOffset) { + const yValues = (data || []).mapBy('percent'); let [low, high] = [0, 1]; if (yValues.compact().length) { [low, high] = d3Array.extent(yValues); } - return d3Scale - .scaleLinear() - .rangeRound([this.xAxisOffset, 10]) + return scaleLinear() + .rangeRound([xAxisOffset, 10]) .domain([Math.min(0, low), Math.max(1, high)]); } } diff --git a/ui/app/templates/components/stats-time-series.hbs b/ui/app/templates/components/stats-time-series.hbs new file mode 100644 index 000000000..69a56fcf0 --- /dev/null +++ b/ui/app/templates/components/stats-time-series.hbs @@ -0,0 +1,12 @@ + diff --git a/ui/tests/integration/components/primary-metric-test.js b/ui/tests/integration/components/primary-metric-test.js index 4cae85321..98746c0a9 100644 --- a/ui/tests/integration/components/primary-metric-test.js +++ b/ui/tests/integration/components/primary-metric-test.js @@ -90,7 +90,7 @@ module('Integration | Component | primary metric', function(hooks) { await render(commonTemplate); assert.ok( - find('[data-test-line-chart] .canvas').classList.contains('is-info'), + find('[data-test-line-chart] .area').classList.contains('is-info'), 'Info class for CPU metric' ); }); @@ -106,7 +106,7 @@ module('Integration | Component | primary metric', function(hooks) { await render(commonTemplate); assert.ok( - find('[data-test-line-chart] .canvas').classList.contains('is-danger'), + find('[data-test-line-chart] .area').classList.contains('is-danger'), 'Danger class for Memory metric' ); }); diff --git a/ui/tests/unit/components/stats-time-series-test.js b/ui/tests/unit/components/stats-time-series-test.js index edee16a78..695e327b4 100644 --- a/ui/tests/unit/components/stats-time-series-test.js +++ b/ui/tests/unit/components/stats-time-series-test.js @@ -3,9 +3,11 @@ import { setupTest } from 'ember-qunit'; import moment from 'moment'; import d3Format from 'd3-format'; import d3TimeFormat from 'd3-time-format'; +import setupGlimmerComponentFactory from 'nomad-ui/tests/helpers/glimmer-factory'; module('Unit | Component | stats-time-series', function(hooks) { setupTest(hooks); + setupGlimmerComponentFactory(hooks, 'stats-time-series'); const ts = (offset, resolution = 'm') => moment() @@ -46,35 +48,29 @@ module('Unit | Component | stats-time-series', function(hooks) { ]; test('xFormat is time-formatted for hours, minutes, and seconds', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); - - chart.set('data', wideData); + const chart = this.createComponent({ data: wideData }); wideData.forEach(datum => { assert.equal( - chart.xFormat()(datum.timestamp), + chart.xFormat(datum.timestamp), d3TimeFormat.timeFormat('%H:%M:%S')(datum.timestamp) ); }); }); test('yFormat is percent-formatted', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); - - chart.set('data', wideData); + const chart = this.createComponent({ data: wideData }); wideData.forEach(datum => { - assert.equal(chart.yFormat()(datum.percent), d3Format.format('.1~%')(datum.percent)); + assert.equal(chart.yFormat(datum.percent), d3Format.format('.1~%')(datum.percent)); }); }); test('x scale domain is at least five minutes', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); - - chart.set('data', narrowData); + const chart = this.createComponent({ data: narrowData }); assert.equal( - +chart.get('xScale').domain()[0], + +chart.xScale(narrowData, 0).domain()[0], +moment(Math.max(...narrowData.mapBy('timestamp'))) .subtract(5, 'm') .toDate(), @@ -83,21 +79,17 @@ module('Unit | Component | stats-time-series', function(hooks) { }); test('x scale domain is greater than five minutes when the domain of the data is larger than five minutes', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); - - chart.set('data', wideData); + const chart = this.createComponent({ data: wideData }); assert.equal( - +chart.get('xScale').domain()[0], + +chart.xScale(wideData, 0).domain()[0], Math.min(...wideData.mapBy('timestamp')), 'The lower bound of the xScale is the oldest timestamp in the dataset' ); }); test('y scale domain is typically 0 to 1 (0 to 100%)', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); - - chart.set('data', wideData); + const chart = this.createComponent({ data: wideData }); assert.deepEqual( [Math.min(...wideData.mapBy('percent')), Math.max(...wideData.mapBy('percent'))], @@ -106,45 +98,41 @@ module('Unit | Component | stats-time-series', function(hooks) { ); assert.deepEqual( - chart.get('yScale').domain(), + chart.yScale(wideData, 0).domain(), [0, 1], 'The bounds of the yScale are still 0 and 1' ); }); test('the extent of the y domain overrides the default 0 to 1 domain when there are values beyond these bounds', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); - - chart.set('data', unboundedData); + const chart = this.createComponent({ data: unboundedData }); assert.deepEqual( - chart.get('yScale').domain(), + chart.yScale(unboundedData, 0).domain(), [-0.5, 1.5], 'The bounds of the yScale match the bounds of the unbounded data' ); - chart.set('data', [unboundedData[0]]); + chart.args.data = [unboundedData[0]]; assert.deepEqual( - chart.get('yScale').domain(), + chart.yScale(chart.args.data, 0).domain(), [-0.5, 1], 'The upper bound is still the default 1, but the lower bound is overridden due to the unbounded low value' ); - chart.set('data', [unboundedData[1]]); + chart.args.data = [unboundedData[1]]; assert.deepEqual( - chart.get('yScale').domain(), + chart.yScale(chart.args.data, 0).domain(), [0, 1.5], 'The lower bound is still the default 0, but the upper bound is overridden due to the unbounded high value' ); }); test('when there are only empty frames in the data array, the default y domain is used', function(assert) { - const chart = this.owner.factoryFor('component:stats-time-series').create(); + const chart = this.createComponent({ data: nullData }); - chart.set('data', nullData); - - assert.deepEqual(chart.get('yScale').domain(), [0, 1], 'The bounds are 0 and 1'); + assert.deepEqual(chart.yScale(nullData, 0).domain(), [0, 1], 'The bounds are 0 and 1'); }); }); From 9e6315e6554b19f61a1e71d9622cc2ae99c3d358 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Feb 2021 17:47:18 -0800 Subject: [PATCH 10/11] Convert ScaleEventsChart into a glimmer component --- ui/app/components/scale-events-chart.js | 25 ++++++++----------- .../components/scale-events-chart-test.js | 10 ++++---- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/ui/app/components/scale-events-chart.js b/ui/app/components/scale-events-chart.js index dadcc821c..f45509319 100644 --- a/ui/app/components/scale-events-chart.js +++ b/ui/app/components/scale-events-chart.js @@ -1,19 +1,17 @@ -import Component from '@ember/component'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { get } from '@ember/object'; import { copy } from 'ember-copy'; -import { computed, get } from '@ember/object'; -import { tagName } from '@ember-decorators/component'; -import classic from 'ember-classic-decorator'; -@classic -@tagName('') export default class ScaleEventsChart extends Component { - events = []; + /** Args + events = [] + */ - activeEvent = null; + @tracked activeEvent = null; - @computed('annotations', 'events.[]') get data() { - const data = this.events.filterBy('hasCount').sortBy('time'); + const data = this.args.events.filterBy('hasCount').sortBy('time'); // Extend the domain of the chart to the current time data.push({ @@ -33,9 +31,8 @@ export default class ScaleEventsChart extends Component { return data; } - @computed('events.[]') get annotations() { - return this.events.rejectBy('hasCount').map(ev => ({ + return this.args.events.rejectBy('hasCount').map(ev => ({ type: ev.error ? 'error' : 'info', time: ev.time, event: copy(ev), @@ -46,11 +43,11 @@ export default class ScaleEventsChart extends Component { if (this.activeEvent && get(this.activeEvent, 'event.uid') === get(ev, 'event.uid')) { this.closeEventDetails(); } else { - this.set('activeEvent', ev); + this.activeEvent = ev; } } closeEventDetails() { - this.set('activeEvent', null); + this.activeEvent = null; } } diff --git a/ui/tests/unit/components/scale-events-chart-test.js b/ui/tests/unit/components/scale-events-chart-test.js index e52b23e33..e0b55c952 100644 --- a/ui/tests/unit/components/scale-events-chart-test.js +++ b/ui/tests/unit/components/scale-events-chart-test.js @@ -1,9 +1,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import sinon from 'sinon'; +import setupGlimmerComponentFactory from 'nomad-ui/tests/helpers/glimmer-factory'; module('Unit | Component | scale-events-chart', function(hooks) { setupTest(hooks); + setupGlimmerComponentFactory(hooks, 'scale-events-chart'); hooks.beforeEach(function() { this.refTime = new Date(); @@ -16,13 +18,12 @@ module('Unit | Component | scale-events-chart', function(hooks) { }); test('the current date is appended as a datum for the line chart to render', function(assert) { - const chart = this.owner.factoryFor('component:scale-events-chart').create(); const events = [ { time: new Date('2020-08-02T04:06:00'), count: 2, hasCount: true }, { time: new Date('2020-08-01T04:06:00'), count: 2, hasCount: true }, ]; - chart.set('events', events); + const chart = this.createComponent({ events }); assert.equal(chart.data.length, events.length + 1); assert.deepEqual(chart.data.slice(0, events.length), events.sortBy('time')); @@ -33,7 +34,6 @@ module('Unit | Component | scale-events-chart', function(hooks) { }); test('if the earliest annotation is outside the domain of the events, the earliest annotation time is added as a datum for the line chart to render', function(assert) { - const chart = this.owner.factoryFor('component:scale-events-chart').create(); const annotationOutside = [ { time: new Date('2020-08-01T04:06:00'), hasCount: false, error: true }, { time: new Date('2020-08-02T04:06:00'), count: 2, hasCount: true }, @@ -45,7 +45,7 @@ module('Unit | Component | scale-events-chart', function(hooks) { { time: new Date('2020-08-03T04:06:00'), count: 2, hasCount: true }, ]; - chart.set('events', annotationOutside); + const chart = this.createComponent({ events: annotationOutside }); assert.equal(chart.data.length, annotationOutside.length + 1); assert.deepEqual( @@ -57,7 +57,7 @@ module('Unit | Component | scale-events-chart', function(hooks) { assert.equal(appendedDatum.count, annotationOutside[1].count); assert.equal(+appendedDatum.time, +annotationOutside[0].time); - chart.set('events', annotationInside); + chart.args.events = annotationInside; assert.equal(chart.data.length, annotationOutside.length); assert.deepEqual( From 85257f6c325bef8703b3abd130b9f2d7921d46e5 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Feb 2021 10:34:02 -0800 Subject: [PATCH 11/11] Update the custom xFormat to be a getter, as is expected by LineChart now --- ui/app/components/line-chart.js | 4 ++++ ui/stories/charts/line-chart.stories.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index 12323685e..71fe03012 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -59,6 +59,10 @@ export default class LineChart extends Component { chartClass = 'is-primary'; activeAnnotation = null; onAnnotationClick() {} + xFormat; + yFormat; + xScale; + yScale; */ @tracked width = 0; diff --git a/ui/stories/charts/line-chart.stories.js b/ui/stories/charts/line-chart.stories.js index e3a1b1fb3..d14cfec09 100644 --- a/ui/stories/charts/line-chart.stories.js +++ b/ui/stories/charts/line-chart.stories.js @@ -112,7 +112,7 @@ export let LiveData = () => { clearInterval(this.timer); }, - secondsFormat() { + get secondsFormat() { return date => moment(date).format('HH:mm:ss'); }, }).create(),