mirror of
				https://github.com/datahub-project/datahub.git
				synced 2025-10-26 16:34:44 +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
	 Charlie Tran
						Charlie Tran