mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-03 06:13:14 +00:00
Merge pull request #1303 from cptran777/score-gauge-component
Score gauge component
This commit is contained in:
commit
fc84868c04
142
wherehows-web/app/components/visualization/charts/score-gauge.ts
Normal file
142
wherehows-web/app/components/visualization/charts/score-gauge.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -2,8 +2,10 @@
|
|||||||
order for each item should always be the same */
|
order for each item should always be the same */
|
||||||
@for $i from 1 through 18 {
|
@for $i from 1 through 18 {
|
||||||
$color: get-dataviz-color($i);
|
$color: get-dataviz-color($i);
|
||||||
|
.viz-chart {
|
||||||
.highcharts-color-#{$i - 1} {
|
.highcharts-color-#{$i - 1} {
|
||||||
fill: $color;
|
fill: $color;
|
||||||
stroke: $color;
|
stroke: $color;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
@import 'bar-chart';
|
@import 'bar-chart';
|
||||||
|
@import 'score-gauge';
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -7,3 +7,62 @@ export interface IChartDatum {
|
|||||||
isFaded?: boolean;
|
isFaded?: boolean;
|
||||||
customColorClass?: string;
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -22,12 +22,13 @@ module.exports = function(defaults) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
emberHighCharts: {
|
emberHighCharts: {
|
||||||
includedHighCharts: true,
|
includeHighCharts: true,
|
||||||
// Note: Since we only need highcharts, excluding the other available modules in the addon
|
// Note: Since we only need highcharts, excluding the other available modules in the addon
|
||||||
includeHighStock: false,
|
includeHighStock: false,
|
||||||
includeHighMaps: false,
|
includeHighMaps: false,
|
||||||
includeHighChartsMore: false,
|
includeHighChartsMore: true,
|
||||||
includeHighCharts3D: false
|
includeHighCharts3D: false,
|
||||||
|
includeModules: ['solid-gauge']
|
||||||
},
|
},
|
||||||
|
|
||||||
storeConfigInMeta: false,
|
storeConfigInMeta: false,
|
||||||
|
@ -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');
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user