Merge pull request #1303 from cptran777/score-gauge-component

Score gauge component
This commit is contained in:
Charlie Tran 2018-08-07 15:38:21 -07:00 committed by GitHub
commit fc84868c04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 369 additions and 6 deletions

View File

@ -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<string>}
*/
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<string>}
*/
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<string>}
*/
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<number>}
*/
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<IHighChartsGaugeConfig>}
*/
chartOptions: IHighChartsGaugeConfig;
/**
* Creates a fresh copy of the data object in the format expected by the highcharts "content" reader.
*/
chartData: Array<IHighChartsDataConfig>;
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
});
}
}

View File

@ -0,0 +1,59 @@
import { IHighChartsGaugeConfig, IHighChartsDataConfig } from 'wherehows-web/typings/app/visualization/charts';
export function getBaseChartDataConfig(name: string): Array<IHighChartsDataConfig> {
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
}
}
}
};
}

View File

@ -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;
}
}
}

View File

@ -1 +1,2 @@
@import 'bar-chart';
@import 'score-gauge';

View File

@ -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;
}
}

View File

@ -0,0 +1,15 @@
{{high-charts chartOptions=chartOptions content=chartData}}
<div class="score-gauge__legend-wrapper">
<h6 class="score-gauge__legend-title">
{{title}}
</h6>
<span class="score-gauge__legend-value {{labelValueClass}}">
{{#if (eq scoreDisplay "percent")}}
{{scoreAsPercentage}}%
{{else if (eq scoreDisplay "outOf")}}
{{score}} / {{maxScore}}
{{else}}
{{score}}
{{/if}}
</span>
</div>

View File

@ -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<string>;
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<Array<string | number>>;
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<number>;
dataLabels?: {
format?: string;
};
}

View File

@ -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,

View File

@ -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');
});