diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index e8846026d2e0..52c5de96427d 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -37,6 +37,9 @@ export default Component.extend(WindowResizable, { timeseries: false, chartClass: 'is-primary', + title: 'Line Chart', + description: null, + // Private Properties width: 0, @@ -96,6 +99,22 @@ export default Component.extend(WindowResizable, { return scale; }), + xRange: computed('data.[]', 'xFormat', 'xProp', 'timeseries', function() { + const { xProp, timeseries, data } = this.getProperties('xProp', 'timeseries', 'data'); + const range = d3Array.extent(data, d => d[xProp]); + const formatter = this.xFormat(timeseries); + + return range.map(formatter); + }), + + yRange: computed('data.[]', 'yFormat', 'yProp', function() { + const yProp = this.get('yProp'); + const range = d3Array.extent(this.get('data'), d => d[yProp]); + const formatter = this.yFormat(); + + return range.map(formatter); + }), + yScale: computed('data.[]', 'yProp', 'xAxisOffset', function() { const yProp = this.get('yProp'); let max = d3Array.max(this.get('data'), d => d[yProp]) || 1; diff --git a/ui/app/components/stats-time-series.js b/ui/app/components/stats-time-series.js index 730762171826..a93a8190a9a2 100644 --- a/ui/app/components/stats-time-series.js +++ b/ui/app/components/stats-time-series.js @@ -5,6 +5,7 @@ import d3Format from 'd3-format'; import d3Scale from 'd3-scale'; import d3Array from 'd3-array'; import LineChart from 'nomad-ui/components/line-chart'; +import formatDuration from 'nomad-ui/utils/format-duration'; export default LineChart.extend({ xProp: 'timestamp', @@ -19,6 +20,20 @@ export default LineChart.extend({ return d3Format.format('.1~%'); }, + // Specific a11y descriptors + title: 'Stats Time Series Chart', + + description: computed('data.[]', 'xProp', 'yProp', function() { + const { xProp, yProp, data } = this.getProperties('data', 'xProp', 'yProp'); + const yRange = d3Array.extent(data, d => d[yProp]); + const xRange = d3Array.extent(data, d => d[xProp]); + const yFormatter = this.yFormat(); + + const duration = formatDuration(xRange[1] - xRange[0], 'ms', true); + + return `Time series data for the last ${duration}, with values ranging from ${yFormatter(yRange[0])} to ${yFormatter(yRange[1])}`; + }), + xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() { const xProp = this.get('xProp'); const scale = this.get('timeseries') ? d3Scale.scaleTime() : d3Scale.scaleLinear(); diff --git a/ui/app/helpers/format-duration.js b/ui/app/helpers/format-duration.js index c85a14b41e91..8ff4f73f3423 100644 --- a/ui/app/helpers/format-duration.js +++ b/ui/app/helpers/format-duration.js @@ -1,8 +1,8 @@ import Helper from '@ember/component/helper'; import formatDuration from '../utils/format-duration'; -function formatDurationHelper([duration], { units }) { - return formatDuration(duration, units); +function formatDurationHelper([duration], { units, longForm }) { + return formatDuration(duration, units, longForm); } export default Helper.helper(formatDurationHelper); diff --git a/ui/app/templates/components/line-chart.hbs b/ui/app/templates/components/line-chart.hbs index eb993918d54f..77861245b729 100644 --- a/ui/app/templates/components/line-chart.hbs +++ b/ui/app/templates/components/line-chart.hbs @@ -1,4 +1,13 @@ - + + {{title}} + + {{#if description}} + {{description}} + {{else}} + X-axis values range from {{xRange.firstObject}} to {{xRange.lastObject}}, + and Y-axis values range from {{yRange.firstObject}} to {{yRange.lastObject}}. + {{/if}} + @@ -14,8 +23,8 @@ - - + +

diff --git a/ui/app/utils/format-duration.js b/ui/app/utils/format-duration.js index 2f8ea1b72b26..11856bf4421b 100644 --- a/ui/app/utils/format-duration.js +++ b/ui/app/utils/format-duration.js @@ -1,18 +1,52 @@ import moment from 'moment'; +/** + * Metadata for all unit types + * name: identifier for the unit. Also maps to moment methods when applicable + * suffix: the preferred suffix for a unit + * inMoment: whether or not moment can be used to compute this unit value + * pluralizable: whether or not this suffix can be pluralized + * longSuffix: the suffix to use instead of suffix when longForm is true + */ const allUnits = [ { name: 'years', suffix: 'year', inMoment: true, pluralizable: true }, { name: 'months', suffix: 'month', inMoment: true, pluralizable: true }, { name: 'days', suffix: 'day', inMoment: true, pluralizable: true }, - { name: 'hours', suffix: 'h', inMoment: true, pluralizable: false }, - { name: 'minutes', suffix: 'm', inMoment: true, pluralizable: false }, - { name: 'seconds', suffix: 's', inMoment: true, pluralizable: false }, + { name: 'hours', suffix: 'h', longSuffix: 'hour', inMoment: true, pluralizable: false }, + { name: 'minutes', suffix: 'm', longSuffix: 'minute', inMoment: true, pluralizable: false }, + { name: 'seconds', suffix: 's', longSuffix: 'second', inMoment: true, pluralizable: false }, { name: 'milliseconds', suffix: 'ms', inMoment: true, pluralizable: false }, { name: 'microseconds', suffix: 'µs', inMoment: false, pluralizable: false }, { name: 'nanoseconds', suffix: 'ns', inMoment: false, pluralizable: false }, ]; -export default function formatDuration(duration = 0, units = 'ns') { +const pluralizeUnits = (amount, unit, longForm) => { + let suffix; + + if (longForm && unit.longSuffix) { + // Long form means always using full words (seconds insteand of s) which means + // pluralization is necessary. + suffix = amount === 1 ? unit.longSuffix : unit.longSuffix.pluralize(); + } else { + // In the normal case, only pluralize based on the pluralizable flag + suffix = amount === 1 || !unit.pluralizable ? unit.suffix : unit.suffix.pluralize(); + } + + // A space should go between the value and the unit when the unit is a full word + // 300ns vs. 1 hour + const addSpace = unit.pluralizable || (longForm && unit.longSuffix); + return `${amount}${addSpace ? ' ' : ''}${suffix}`; +}; + +/** + * Format a Duration at a preferred precision + * + * @param {Number} duration The duration to format + * @param {String} units The units for the duration. Default to nanoseconds. + * @param {Boolean} longForm Whether or not to expand single character suffixes, + * used to ensure screen readers correctly read units. + */ +export default function formatDuration(duration = 0, units = 'ns', longForm = false) { const durationParts = {}; // Moment only handles up to millisecond precision. @@ -46,9 +80,7 @@ export default function formatDuration(duration = 0, units = 'ns') { const displayParts = allUnits.reduce((parts, unitType) => { if (durationParts[unitType.name]) { const count = durationParts[unitType.name]; - const suffix = - count === 1 || !unitType.pluralizable ? unitType.suffix : unitType.suffix.pluralize(); - parts.push(`${count}${unitType.pluralizable ? ' ' : ''}${suffix}`); + parts.push(pluralizeUnits(count, unitType, longForm)); } return parts; }, []); @@ -58,7 +90,5 @@ export default function formatDuration(duration = 0, units = 'ns') { } // When the duration is 0, show 0 in terms of `units` - const unitTypeForUnits = allUnits.findBy('suffix', units); - const suffix = unitTypeForUnits.pluralizable ? units.pluralize() : units; - return `0${unitTypeForUnits.pluralizable ? ' ' : ''}${suffix}`; + return pluralizeUnits(0, allUnits.findBy('suffix', units), longForm); } diff --git a/ui/tests/unit/utils/format-duration-test.js b/ui/tests/unit/utils/format-duration-test.js index c4867590f32d..69cb02b32252 100644 --- a/ui/tests/unit/utils/format-duration-test.js +++ b/ui/tests/unit/utils/format-duration-test.js @@ -26,3 +26,10 @@ test('When duration is 0, 0 is shown in terms of the units provided to the funct assert.equal(formatDuration(0), '0ns', 'formatDuration(0) -> 0ns'); assert.equal(formatDuration(0, 'year'), '0 years', 'formatDuration(0, "year") -> 0 years'); }); + +test('The longForm option expands suffixes to words', function(assert) { + const expectation1 = '3 seconds 20ms'; + const expectation2 = '5 hours 59 minutes'; + assert.equal(formatDuration(3020, 'ms', true), expectation1, expectation1); + assert.equal(formatDuration(60 * 5 + 59, 'm', true), expectation2, expectation2); +});