mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-01 11:19:05 +00:00
174 lines
5.9 KiB
TypeScript
174 lines
5.9 KiB
TypeScript
import Component from '@ember/component';
|
|
import { IChartDatum } from 'wherehows-web/typings/app/visualization/charts';
|
|
import { computed, get, set, setProperties } from '@ember/object';
|
|
import ComputedProperty from '@ember/object/computed';
|
|
|
|
interface IBarSeriesDatum extends IChartDatum {
|
|
yOffset: number;
|
|
barLength: number;
|
|
labelOffset: number;
|
|
}
|
|
|
|
/**
|
|
* This custom component exists outside of highcharts as the library does not provide the amount
|
|
* of capabilities we need to match up with our design vision for horizontal bar charts. As such,
|
|
* there are similarities between this component and a highcharts component but it has been
|
|
* tailor-fit to our needs
|
|
*
|
|
* Bar Chart Usage
|
|
* {{visualization/charts/horizontal-bar-chart
|
|
* series=[ { name: string, value: number, otherKey: otherValue } ]
|
|
* title="string"
|
|
* labelTagProperty="optionStringOverridesDefault"
|
|
* labelAppendTag="optionalStringAppendsEachTag"
|
|
* labelAppendValue="optionalStringSuchAs%"}}
|
|
*/
|
|
export default class HorizontalBarChart extends Component {
|
|
/**
|
|
* Sets the tag for the rendered html elemenet for the component
|
|
* @type {string}
|
|
*/
|
|
tagName = 'figure';
|
|
|
|
/**
|
|
* Sets the classes for the rendered html element for the component
|
|
* @type {Array<string>}
|
|
*/
|
|
classNames = ['vz-chart', 'viz-bar-chart', 'single-series'];
|
|
|
|
/**
|
|
* Represents the series of data needed to power our chart. Format is
|
|
* [ { name: string, value: number } ].
|
|
* Since this chart is only meant to handle a single series of data where each bar is connected
|
|
* to one value with one label, we don't have to worry about the idea of an "x axis * y axis"
|
|
* @type {Array<IChartDatum>}
|
|
*/
|
|
series: Array<IChartDatum>;
|
|
|
|
/**
|
|
* Helps to set the size of the svg element rendered by the component
|
|
* @type {number}
|
|
*/
|
|
size: number = 0;
|
|
|
|
/**
|
|
* Property in the series datum to use as the tag for each value in the bar legend. Note, each
|
|
* legend item will appear as VALUE | TAG
|
|
* @type {string}
|
|
* @default 'name'
|
|
*/
|
|
labelTagProperty: string;
|
|
|
|
/**
|
|
* Any string we want to append to each tag in the label, such as a unit.
|
|
* @type {string}
|
|
*/
|
|
labelAppendTag: string;
|
|
|
|
/**
|
|
* Any string that we want to append to each value in the label, such as %. Doing so would
|
|
* append every value, such as 60, in the label with % and appear as 60%
|
|
* @type {string}
|
|
*/
|
|
labelAppendValue: string;
|
|
|
|
/**
|
|
* Constant properties to be used in calculations for the size of the svg elements drawn
|
|
* @type {number}
|
|
*/
|
|
BAR_HEIGHT = 16;
|
|
BAR_MARGIN_BOTTOM = 8;
|
|
LABEL_HEIGHT = 15;
|
|
LABEL_MARGIN_BOTTOM = 16;
|
|
|
|
/**
|
|
* Overall width of our chart. If we have a size, that means that the component and available space
|
|
* has been measured.
|
|
* @type {ComputedProperty<number>}
|
|
*/
|
|
width: ComputedProperty<number> = computed('size', function(this: HorizontalBarChart) {
|
|
return get(this, 'size') ? this.$(this.element).width() : 0;
|
|
});
|
|
|
|
/**
|
|
* Overall height of our chart, calculated based on the amount of items we have in our series
|
|
* @type {ComputedProperty<number>}
|
|
*/
|
|
height: ComputedProperty<number> = computed('categories', function(this: HorizontalBarChart) {
|
|
return (get(this, 'series') || []).length * this.heightModifier();
|
|
});
|
|
|
|
/**
|
|
* Calculates information needed for the svg element to properly render each bar of our graph using the
|
|
* correct dimensions relative to the data it's receiving
|
|
* @type {ComputedProperty<IBarSeriesDatum[]}
|
|
*/
|
|
seriesData: ComputedProperty<Array<IBarSeriesDatum>> = computed('series', 'size', function(this: HorizontalBarChart) {
|
|
return (this.get('series') || []).map(this.bar.bind(this));
|
|
});
|
|
|
|
/**
|
|
* Sets our highest value for the chart's Y axis, based on the highest value inside the series
|
|
* @type {ComputedProperty<number>}
|
|
*/
|
|
maxY: ComputedProperty<number> = computed('series', function(this: HorizontalBarChart) {
|
|
return (get(this, 'series') || []).reduce((memo, dataPoint) => {
|
|
if (dataPoint.value > memo) {
|
|
return dataPoint.value;
|
|
}
|
|
return memo;
|
|
}, Number.MIN_VALUE);
|
|
});
|
|
|
|
/**
|
|
* Returns a "modifier" that is the height of a single bar and label in the chart, and can be multiplied
|
|
* by the number of rows in the chart to get the total chart height
|
|
* @param this - explicit this keyword declaration for typescript
|
|
*/
|
|
heightModifier(this: HorizontalBarChart): number {
|
|
return (
|
|
get(this, 'BAR_HEIGHT') +
|
|
get(this, 'BAR_MARGIN_BOTTOM') +
|
|
get(this, 'LABEL_HEIGHT') +
|
|
get(this, 'LABEL_MARGIN_BOTTOM')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Used as a predicate function in the mapping function for the series array to be mapped into the
|
|
* seriesData array, this function adds values to each chart datum object so that the svg template
|
|
* can render each bar with the correct dimensions and position
|
|
* @param this - explicit this keyword declaration for typescript
|
|
* @param data - single datum object in our series
|
|
* @param index - current index in the series array
|
|
*/
|
|
bar(this: HorizontalBarChart, data: IChartDatum, index: number) {
|
|
const yOffset = 1 + index * this.heightModifier();
|
|
|
|
return {
|
|
...data,
|
|
yOffset,
|
|
barLength: Math.max(1, Math.floor(data.value / get(this, 'maxY') * get(this, 'width'))),
|
|
labelOffset: yOffset + get(this, 'BAR_HEIGHT') + get(this, 'BAR_MARGIN_BOTTOM') + get(this, 'LABEL_HEIGHT')
|
|
};
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
// Applying passed in properties or setting to default values
|
|
setProperties(this, {
|
|
labelTagProperty: this.labelTagProperty || 'name',
|
|
labelAppendTag: this.labelAppendTag || '',
|
|
labelAppendValue: this.labelAppendValue || ''
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Once we have inserted our html element, we can determine the width (size) of our chart
|
|
*/
|
|
didInsertElement() {
|
|
this._super(...arguments);
|
|
set(this, 'size', this.$(this.element).width() || 0);
|
|
}
|
|
}
|