diff --git a/wherehows-web/app/components/visualization/charts/score-gauge.ts b/wherehows-web/app/components/visualization/charts/score-gauge.ts new file mode 100644 index 0000000000..7be5c597ee --- /dev/null +++ b/wherehows-web/app/components/visualization/charts/score-gauge.ts @@ -0,0 +1,142 @@ +import Component from '@ember/component'; +import { computed, setProperties, getProperties, get } from '@ember/object'; +import { IHighChartsGaugeConfig, IHighChartsDataConfig } from 'wherehows-web/typings/app/visualization/charts'; +import { getBaseGaugeConfig, getBaseChartDataConfig } from 'wherehows-web/constants/visualization/charts/chart-configs'; + +/** + * Whether the score is in the good (51%+), warning (26-50%) or critical (0-25% range) + */ +export enum ScoreState { + good = 'good', + warning = 'warning', + critical = 'critical' +} + +/** + * How we want to display our score. If our score is 166 and max score is 200, percentage will display as + * 83%, outOf will display as 166 / 200, and number will display as 166 + */ +export enum ScoreDisplay { + percentage = 'percent', + outOf = 'outOf', + number = 'number' +} + +/** + * This score gauge component was originally developed to handle showing metadata health score gauges for a + * particular dataset. It appears as a basic circle gauge that changes colors depending on how far along we + * are in terms of score "percentage" and includes a simple legend and value display. There are currently + * no user interactions with this component. + * + * @example + * {{visualization/charts/score-gauge + * title="string" + * score=numberValue + * maxScore=optionalNumberValue + * scoreDisplay="percent" // Optional, defaults to "percent" but values can also be "outOf" and "number", + * // see details in class definition + * }} + */ +export default class VisualizationChartsScoreGauge extends Component { + /** + * Sets the classes for the rendered html element for the component + * @type {Array} + */ + classNames = ['score-gauge']; + + /** + * Displays a passed in chart title. + * @type {number} + * @default '' + */ + title: string; + + /** + * Fetched score data in order to render onto the graph + * @type {number} + * @default 0 + */ + score: number; + + /** + * Represents the maximum value a score can be. Helps us to calculate a percentage score + * @type {number} + * @default 100 + */ + maxScore: number; + + /** + * Format option to determine how to display our score in the legend label + * @type {ScoreDisplay} + * @default ScoreDisplay.percentage + */ + scoreDisplay: ScoreDisplay; + + /** + * Gives a simple access to the chart state for other computed values to use + * @type {ComputedProperty} + */ + chartState = computed('score', function(): ScoreState { + const scoreAsPercentage = get(this, 'scoreAsPercentage'); + + if (scoreAsPercentage <= 25) { + return ScoreState.critical; + } else if (scoreAsPercentage <= 50) { + return ScoreState.warning; + } + + return ScoreState.good; + }); + + /** + * Computes the class to properly color the legend value between the different states + * @type {ComputedProperty} + */ + labelValueClass = computed('chartState', function(): string { + return `score-gauge__legend-value--${get(this, 'chartState')}`; + }); + + /** + * Computes the score as a percentage in order to determine the score state property as well as use + * in the template to display the numerical score if we choose to display as a percentage + * @type {ComputedProperty} + */ + scoreAsPercentage = computed('score', function(): number { + const { score, maxScore } = getProperties(this, 'score', 'maxScore'); + + return Math.round((score / maxScore) * 100); + }); + + /** + * Creates a fresh configuration for our gauge chart every time we init a new instance of this + * component class + * @type {ComputedProperty} + */ + chartOptions: IHighChartsGaugeConfig; + + /** + * Creates a fresh copy of the data object in the format expected by the highcharts "content" reader. + */ + chartData: Array; + + constructor() { + super(...arguments); + + const chartOptions = getBaseGaugeConfig(); + const chartData = getBaseChartDataConfig('score'); + const maxScore = typeof this.maxScore === 'number' ? this.maxScore : 100; + const score = this.score || NaN; + // Adds our information to the highcharts formatted configurations so that they can be read in the chart + chartOptions.yAxis.max = maxScore; + chartData[0].data = [score]; + + setProperties(this, { + score, + maxScore, + chartOptions, + chartData, + title: this.title || '', + scoreDisplay: this.scoreDisplay || ScoreDisplay.percentage + }); + } +} diff --git a/wherehows-web/app/constants/visualization/charts/chart-configs.ts b/wherehows-web/app/constants/visualization/charts/chart-configs.ts new file mode 100644 index 0000000000..b2cacf5d69 --- /dev/null +++ b/wherehows-web/app/constants/visualization/charts/chart-configs.ts @@ -0,0 +1,59 @@ +import { IHighChartsGaugeConfig, IHighChartsDataConfig } from 'wherehows-web/typings/app/visualization/charts'; + +export function getBaseChartDataConfig(name: string): Array { + return [{ name, data: [0] }]; +} + +export function getBaseGaugeConfig(): IHighChartsGaugeConfig { + return { + chart: { type: 'solidgauge', backgroundColor: 'transparent' }, + title: '', + pane: { + center: ['50%', '50%'], + size: '100%', + startAngle: 0, + endAngle: 360, + background: { + backgroundColor: '#ddd', + innerRadius: '90%', + outerRadius: '100%', + shape: 'arc', + borderColor: 'transparent' + } + }, + tooltip: { + enabled: false + }, + yAxis: { + min: 0, + max: 100, + stops: [ + [0.25, '#ff2c33'], // get-color(red5) + [0.5, '#e55800'], // get-color(orange5) + [0.75, '#469a1f'] // get-color(green5) + ], + minorTickInterval: null, + tickPixelInterval: 400, + tickWidth: 0, + gridLineWidth: 0, + gridLineColor: 'transparent', + labels: { + enabled: false + }, + title: { + enabled: false + } + }, + credits: { + enabled: false + }, + plotOptions: { + solidgauge: { + innerRadius: '90%', + dataLabels: { + enabled: false + } + } + } + }; +} diff --git a/wherehows-web/app/styles/base/_visualization.scss b/wherehows-web/app/styles/base/_visualization.scss index 9f7c540162..f2ede20947 100644 --- a/wherehows-web/app/styles/base/_visualization.scss +++ b/wherehows-web/app/styles/base/_visualization.scss @@ -2,8 +2,10 @@ order for each item should always be the same */ @for $i from 1 through 18 { $color: get-dataviz-color($i); - .highcharts-color-#{$i - 1} { - fill: $color; - stroke: $color; + .viz-chart { + .highcharts-color-#{$i - 1} { + fill: $color; + stroke: $color; + } } } diff --git a/wherehows-web/app/styles/components/visualization/charts/_all.scss b/wherehows-web/app/styles/components/visualization/charts/_all.scss index 771e6fb151..9026ecdfbc 100644 --- a/wherehows-web/app/styles/components/visualization/charts/_all.scss +++ b/wherehows-web/app/styles/components/visualization/charts/_all.scss @@ -1 +1,2 @@ @import 'bar-chart'; +@import 'score-gauge'; diff --git a/wherehows-web/app/styles/components/visualization/charts/_score-gauge.scss b/wherehows-web/app/styles/components/visualization/charts/_score-gauge.scss new file mode 100644 index 0000000000..74c0a25b5f --- /dev/null +++ b/wherehows-web/app/styles/components/visualization/charts/_score-gauge.scss @@ -0,0 +1,41 @@ +$score-gauge-dimension: 128px; + +.score-gauge { + display: flex; + + &__legend { + &-wrapper { + display: flex; + flex-direction: column; + justify-content: center; + } + + &-title { + font-size: 16px; + font-weight: 600; + margin: 0; + } + + &-value { + font-size: 28px; + font-weight: 400; + + &--good { + color: get-color(green5); + } + + &--warning { + color: get-color(orange5); + } + + &--critical { + color: get-color(red5); + } + } + } + + .chart-container { + height: $score-gauge-dimension; + width: $score-gauge-dimension; + } +} diff --git a/wherehows-web/app/templates/components/visualization/charts/score-gauge.hbs b/wherehows-web/app/templates/components/visualization/charts/score-gauge.hbs new file mode 100644 index 0000000000..8ad1b508c0 --- /dev/null +++ b/wherehows-web/app/templates/components/visualization/charts/score-gauge.hbs @@ -0,0 +1,15 @@ +{{high-charts chartOptions=chartOptions content=chartData}} +
+
+ {{title}} +
+ + {{#if (eq scoreDisplay "percent")}} + {{scoreAsPercentage}}% + {{else if (eq scoreDisplay "outOf")}} + {{score}} / {{maxScore}} + {{else}} + {{score}} + {{/if}} + +
\ No newline at end of file diff --git a/wherehows-web/app/typings/app/visualization/charts.d.ts b/wherehows-web/app/typings/app/visualization/charts.d.ts index 0817a3b1d6..16199318da 100644 --- a/wherehows-web/app/typings/app/visualization/charts.d.ts +++ b/wherehows-web/app/typings/app/visualization/charts.d.ts @@ -7,3 +7,62 @@ export interface IChartDatum { isFaded?: boolean; customColorClass?: string; } + +/** + * Expected parameters for a high charts configuration object. This will probably be expanded as + * we deal with more charts and use cases but the starting generic point is based on gauges + */ +export interface IHighChartsConfig { + chart: { type: string; backgroundColor?: string }; + title?: string; + pane?: { + center?: Array; + size?: string; + startAngle?: number; + endAngle?: number; + background?: { + backgroundColor?: string; + innerRadius?: string; + outerRadius?: string; + shape?: string; + borderColor?: string; + }; + }; + tooltip?: { enabled?: boolean }; + plotOptions?: any; +} + +/** + * Expected parameters for a high charts solid gauge configuration object. This may be expanded as + * we deal with more gauge use cases + */ +export interface IHighChartsGaugeConfig extends IHighChartsConfig { + yAxis: { + min: number; + max: number; + stops: Array>; + minorTickInterval?: any; + tickPixelInterval: number; + tickWidth: number; + gridLineWidth: number; + gridLineColor: string; + labels?: { enabled?: boolean }; + title?: { enabled?: boolean }; + }; + credits?: { enabled?: boolean }; + plotOptions: { + solidgauge: { + innerRadius?: string; + dataLabels?: { enabled?: boolean }; + }; + }; +} + +export interface IHighChartsDataConfig { + name?: string; + // May need to be refined as we develop new kinds of charts + data: Array; + dataLabels?: { + format?: string; + }; +} diff --git a/wherehows-web/ember-cli-build.js b/wherehows-web/ember-cli-build.js index 0bf4f90857..7d71d1f4e5 100644 --- a/wherehows-web/ember-cli-build.js +++ b/wherehows-web/ember-cli-build.js @@ -22,12 +22,13 @@ module.exports = function(defaults) { }, emberHighCharts: { - includedHighCharts: true, + includeHighCharts: true, // Note: Since we only need highcharts, excluding the other available modules in the addon includeHighStock: false, includeHighMaps: false, - includeHighChartsMore: false, - includeHighCharts3D: false + includeHighChartsMore: true, + includeHighCharts3D: false, + includeModules: ['solid-gauge'] }, storeConfigInMeta: false, diff --git a/wherehows-web/tests/integration/components/visualization/charts/score-gauge-test.js b/wherehows-web/tests/integration/components/visualization/charts/score-gauge-test.js new file mode 100644 index 0000000000..d81c071dad --- /dev/null +++ b/wherehows-web/tests/integration/components/visualization/charts/score-gauge-test.js @@ -0,0 +1,43 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('visualization/charts/score-gauge', 'Integration | Component | visualization/charts/score-gauge', { + integration: true +}); + +const chartContainer = '.score-gauge'; +const legendTitle = `${chartContainer}__legend-title`; +const legendValue = `${chartContainer}__legend-value`; + +test('it renders', async function(assert) { + this.render(hbs`{{visualization/charts/score-gauge}}`); + assert.ok(this.$(), 'Renders without errors'); +}); + +test('it renders the correct inforamtion', async function(assert) { + const score = 85; + const title = 'Ash Ketchum'; + this.setProperties({ score, title }); + + this.render(hbs`{{visualization/charts/score-gauge + score=score + title=title}}`); + + assert.ok(this.$(), 'Still renders without erorrs'); + assert.equal(this.$(chartContainer).length, 1, 'Renders one correct element'); + assert.equal( + this.$(legendTitle) + .text() + .trim(), + title, + 'Renders the correct title' + ); + assert.equal( + this.$(legendValue) + .text() + .trim(), + `${score}%`, + 'Renders the score in the correct format' + ); + assert.equal(this.$(`${legendValue}--good`).length, 1, 'Renders the score in the correct styling'); +});