mirror of
https://github.com/datahub-project/datahub.git
synced 2025-09-02 22:03:11 +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 */
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
@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;
|
||||
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: {
|
||||
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,
|
||||
|
@ -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