diff --git a/public/assets/css/components.css b/public/assets/css/components.css index aa843663..c3058c58 100644 --- a/public/assets/css/components.css +++ b/public/assets/css/components.css @@ -243,7 +243,6 @@ div.g-pickers-container div.g-picker { /*---Fin Graphic---*/ - /*---Card---*/ /*-General Styles-*/ @@ -986,6 +985,16 @@ main a[href^="https://"][target^="_blank"]::after { /*---Fin PreviewCard---*/ +/*---CardRow---*/ + +div.r-row { + display: flex; + justify-content: space-around; + margin-bottom: 40px; +} + +/*---Fin CardRow---*/ + /*---Iconos---*/ .fa-angle-up, .fa-link, .caret { diff --git a/src/api/Serie.ts b/src/api/Serie.ts index 675e565b..3cf6c656 100644 --- a/src/api/Serie.ts +++ b/src/api/Serie.ts @@ -224,6 +224,7 @@ export default class Serie implements ISerie { units: this.units, }; } + } function emptyDatasetThemes(): IDataSetTheme[] { diff --git a/src/components/exportable/CardExportable.tsx b/src/components/exportable/CardExportable.tsx index 47d486ec..3596362e 100644 --- a/src/components/exportable/CardExportable.tsx +++ b/src/components/exportable/CardExportable.tsx @@ -71,7 +71,6 @@ export default class CardExportable extends React.Component { + + private seriesApi: SerieApi; + private idsAmount: number; + + private colors: string[]; + private decimals: number[]; + private decimalsBillion: number[]; + private decimalsMillion: number[]; + private explicitSigns: boolean[]; + private numbersAbbreviate: boolean[]; + private sources: string[]; + private titles: string[]; + private units: string[]; + + public constructor(props: ICardRowExportableProps) { + super(props); + + this.seriesApi = new SerieApi(new ApiClient(props.apiBaseUrl || getAPIDefaultURI(), 'ts-components-row')); + this.idsAmount = this.props.ids.length; + + this.colors = []; + this.decimals = []; + this.decimalsBillion = []; + this.decimalsMillion = []; + this.explicitSigns = []; + this.numbersAbbreviate = []; + this.sources = []; + this.titles = []; + this.units = []; + + this.checkColor(); + this.checkDecimals(); + this.checkDecimalsBillion(); + this.checkDecimalsMillion(); + this.checkExplicitSign(); + this.checkNumbersAbbreviate(); + this.checkSource(); + this.checkTitle(); + this.checkUnits(); + + this.state = { + series: null + } + } + + public componentDidMount() { + const params = new QueryParams(this.props.ids); + if(this.props.collapse !== undefined) { + params.setCollapse(this.props.collapse); + } + params.setLast(5000); + params.setMetadata(METADATA.FULL); + this.fetchSeries(params); + } + + public render() { + + if (!this.state.series) { return null; } + + const rowsAmount = Math.floor(this.idsAmount / 4) + 1; + const rows = []; + + for (let i = 0; i < rowsAmount; i++) { + + const cards = []; + for (let j = i*4; j < (i+1)*4 && j < this.idsAmount; j++) { + + const downloadUrl = this.getDownloadUrl(this.props.ids[j]); + const laps = LAPS[this.state.series[j].accrualPeriodicity]; + const cardOptions: ICardBaseConfig = { + chartType: "line", + collapse: this.props.collapse, + color: this.colors[j], + decimals: this.decimals[j], + decimalsBillion: this.decimalsBillion[j], + decimalsMillion: this.decimalsMillion[j], + explicitSign: this.explicitSigns[j], + hasChart: this.props.hasChart, + hasColorBar: this.props.hasColorBar, + hasFrame: this.props.hasFrame, + isPercentage: this.props.isPercentage, + links: this.props.links, + locale: this.props.locale, + numbersAbbreviate: this.numbersAbbreviate[j], + source: this.sources[j], + title: this.titles[j], + units: this.units[j] + } + + cards.push(); + + } + + rows.push(
{cards}
) + } + + return( +
+ {rows} +
+ ); + + } + + private checkColor() { + + if (Array.isArray(this.props.color)) { + if(this.props.color.length !== this.props.ids.length) { + throw new Error(`El parametro color debe ser un array de igual longitud al parametro ids, o bien un string solo`); + } + this.colors = this.props.color; + } + else if (this.props.color !== undefined) { + const color = getCardColor(this.props.color); + for (let step = 0; step < this.idsAmount; step++) { + this.colors.push(color); + } + } + else { + for (let step = 0; step < this.idsAmount; step++) { + this.colors.push(DEFAULT_COLORS[step % DEFAULT_COLORS.length].code) + } + } + + } + + private checkDecimals() { + + if (Array.isArray(this.props.decimals)) { + if(this.props.decimals.length !== this.props.ids.length) { + throw new Error(`El parametro decimals debe ser un array de igual longitud al parametro ids, o bien un numero solo`); + } + this.decimals = this.props.decimals; + } + else if (this.props.decimals !== undefined) { + for (let step = 0; step < this.idsAmount; step++) { + this.decimals.push(Math.max(this.props.decimals, 0)); + } + } + + } + + private checkDecimalsBillion() { + + if (Array.isArray(this.props.decimalsBillion)) { + if(this.props.decimalsBillion.length !== this.props.ids.length) { + throw new Error(`El parametro decimalsBillion debe ser un array de igual longitud al parametro ids, o bien un numero solo`); + } + this.decimalsBillion = this.props.decimalsBillion; + } + else if (this.props.decimalsBillion) { + const decimalsBillion = this.props.decimalsBillion !== undefined && this.props.decimalsBillion >= 0 ? this.props.decimalsBillion : DEFAULT_DECIMALS_BILLION; + for (let step = 0; step < this.idsAmount; step++) { + this.decimalsBillion.push(decimalsBillion); + } + } + + } + + private checkDecimalsMillion() { + + if (Array.isArray(this.props.decimalsMillion)) { + if(this.props.decimalsMillion.length !== this.props.ids.length) { + throw new Error(`El parametro decimalsMillion debe ser un array de igual longitud al parametro ids, o bien un numero solo`); + } + this.decimalsMillion = this.props.decimalsMillion; + } + else if (this.props.decimalsMillion) { + const decimalsMillion = this.props.decimalsMillion !== undefined && this.props.decimalsMillion >= 0 ? this.props.decimalsMillion : DEFAULT_DECIMALS_MILLION; + for (let step = 0; step < this.idsAmount; step++) { + this.decimalsMillion.push(decimalsMillion); + } + } + + } + + private checkExplicitSign() { + + if (Array.isArray(this.props.explicitSign)) { + if(this.props.explicitSign.length !== this.props.ids.length) { + throw new Error(`El parametro explicitSign debe ser un array de igual longitud al parametro ids, o bien un booleano solo`); + } + this.explicitSigns = this.props.explicitSign; + } + else { + const explicitSign = this.props.explicitSign !== undefined ? this.props.explicitSign : false; + for (let step = 0; step < this.idsAmount; step++) { + this.explicitSigns.push(explicitSign); + } + } + + } + + private checkNumbersAbbreviate() { + + if (Array.isArray(this.props.numbersAbbreviate)) { + if(this.props.numbersAbbreviate.length !== this.props.ids.length) { + throw new Error(`El parametro numbersAbbreviate debe ser un array de igual longitud al parametro ids, o bien un booleano solo`); + } + this.numbersAbbreviate = this.props.numbersAbbreviate; + } + else { + const numbersAbbreviate = this.props.numbersAbbreviate !== undefined ? this.props.numbersAbbreviate : true; + for (let step = 0; step < this.idsAmount; step++) { + this.numbersAbbreviate.push(numbersAbbreviate); + } + } + + } + + private checkSource() { + + if (Array.isArray(this.props.source)) { + if(this.props.source.length !== this.props.ids.length) { + throw new Error(`El parametro source debe ser un array de igual longitud al parametro ids, o bien un string solo`); + } + this.sources = this.props.source; + } + else { + for (let step = 0; step < this.idsAmount; step++) { + this.sources.push(this.props.source); + } + } + + } + + private checkTitle() { + + if (Array.isArray(this.props.title)) { + if(this.props.title.length !== this.props.ids.length) { + throw new Error(`El parametro title debe ser un array de igual longitud al parametro ids, o bien un string solo`); + } + this.titles = this.props.title; + } + else { + for (let step = 0; step < this.idsAmount; step++) { + this.titles.push(this.props.title); + } + } + + } + + private checkUnits() { + + if (Array.isArray(this.props.units)) { + if(this.props.units.length !== this.props.ids.length) { + throw new Error(`El parametro units debe ser un array de igual longitud al parametro ids, o bien un string solo`); + } + this.units = this.props.units; + } + else { + for (let step = 0; step < this.idsAmount; step++) { + this.units.push(this.props.units); + } + } + + } + + private fetchSeries(params: QueryParams) { + this.seriesApi.fetchSeries(params) + .then((series: ISerie[]) => { + this.setState({ + series + }) + }) + } + + private getDownloadUrl(serieId: string): string { + const params = new QueryParams([serieId]); + params.setLast(5000); + if(this.props.collapse !== undefined) { + params.setCollapse(this.props.collapse); + } + return this.seriesApi.downloadDataURL(params); + } + +} diff --git a/src/components/exportable_card/FullCard.tsx b/src/components/exportable_card/FullCard.tsx index f88debfd..63f76617 100644 --- a/src/components/exportable_card/FullCard.tsx +++ b/src/components/exportable_card/FullCard.tsx @@ -13,6 +13,7 @@ import FullCardValue from '../style/exportable_card/FullCardValue'; import FullCardChart from './FullCardChart'; import FullCardLinks from './FullCardLinks'; import { buildAbbreviationProps } from '../../helpers/common/numberAbbreviation'; +import { lastNonNullPoint } from '../../helpers/common/serieDataHandling'; interface IFullCardProps { @@ -29,7 +30,7 @@ export default (props: IFullCardProps) => { downloadUrl: props.downloadUrl, serieId: getFullSerieId(props.serie) }; - const value = props.serie.data[props.serie.data.length-1].value; + const value = lastNonNullPoint(props.serie.data).value; const abbreviationProps = buildAbbreviationProps(options.numbersAbbreviate, options.decimalsBillion, options.decimalsMillion); const formatterConfig: ILocaleValueFormatterConfig = { code: options.locale, diff --git a/src/components/style/Colors/Color.ts b/src/components/style/Colors/Color.ts index 85bdbc1f..df0f71af 100644 --- a/src/components/style/Colors/Color.ts +++ b/src/components/style/Colors/Color.ts @@ -21,7 +21,7 @@ const COLORS = { const hexaColorRegex = /^#[0-9a-f]{6}$/i; export const DEFAULT_CARD_COLOR = "#0072BB"; -const DEFAULT_COLORS = (Object as any).values(COLORS); +export const DEFAULT_COLORS = (Object as any).values(COLORS); export default COLORS; diff --git a/src/helpers/common/dateFunctions.ts b/src/helpers/common/dateFunctions.ts index 2f6fffe9..99bac995 100644 --- a/src/helpers/common/dateFunctions.ts +++ b/src/helpers/common/dateFunctions.ts @@ -3,6 +3,7 @@ import 'moment/locale/es'; import { ISerie } from "../../api/Serie"; import { PERIODICITY_LANG } from "../../api/utils/periodicityManager"; import { capitalize } from "./commonFunctions"; +import { lastNonNullPoint } from "./serieDataHandling"; export function timestamp(date: string): number { return new Date(date).getTime() @@ -99,6 +100,6 @@ export function shortLocaleDate(format: string, dateString: string) { export function lastSerieDate(serie: ISerie): string { const langFrequency = serie.frequency !== undefined ? PERIODICITY_LANG[serie.frequency] : serie.accrualPeriodicity; - return fullLocaleDate(langFrequency, serie.data[serie.data.length-1].date); + return fullLocaleDate(langFrequency, lastNonNullPoint(serie.data).date); } \ No newline at end of file diff --git a/src/helpers/common/serieDataHandling.ts b/src/helpers/common/serieDataHandling.ts new file mode 100644 index 00000000..0aef8f81 --- /dev/null +++ b/src/helpers/common/serieDataHandling.ts @@ -0,0 +1,13 @@ +import { IDataPoint } from "../../api/DataPoint"; + +export function lastNonNullPoint(datapoints: IDataPoint[]): IDataPoint { + + const valuesAmount = datapoints.length; + for (let i = valuesAmount - 1; i >= 0; i--) { + if(datapoints[i].value !== null) { + return datapoints[i]; + } + } + return datapoints[0]; + +} \ No newline at end of file diff --git a/src/indexCardRow.tsx b/src/indexCardRow.tsx new file mode 100644 index 00000000..b383e26c --- /dev/null +++ b/src/indexCardRow.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import CardRowExportable from "./components/exportable/CardRowExportable"; + +export interface ICardRowExportableConfig { + apiBaseUrl?: string; + collapse?: string; + color?: string[] | string; + decimals?: number[] | number; + decimalsBillion?: number[] | number; + decimalsMillion?: number[] | number; + explicitSign?: boolean[] | boolean; + hasChart: string; + hasColorBar?: boolean; + hasFrame?: boolean; + ids: string[]; + isPercentage?: boolean; + links: string; + locale: string; + numbersAbbreviate?: boolean[] | boolean; + source: string[] | string; + title: string[] | string; + units: string[] | string; +} + +export function render(selector: string, config: ICardRowExportableConfig) { + + ReactDOM.render( + , + document.getElementById(selector) as HTMLElement + ) + +} diff --git a/src/indexComponents.tsx b/src/indexComponents.tsx index 3731c200..79aa712d 100644 --- a/src/indexComponents.tsx +++ b/src/indexComponents.tsx @@ -1,8 +1,10 @@ import * as Card from './indexCard'; import * as Graphic from './indexGraphic'; import * as PreviewCard from './indexPreviewCard'; +import * as CardRow from './indexCardRow'; export { Graphic }; export { Card }; export { PreviewCard }; +export { CardRow }; diff --git a/src/tests/components/helpers/SerieDataHandling.test.ts b/src/tests/components/helpers/SerieDataHandling.test.ts new file mode 100644 index 00000000..bfbbdfaf --- /dev/null +++ b/src/tests/components/helpers/SerieDataHandling.test.ts @@ -0,0 +1,46 @@ +import { IDataPoint } from "../../../api/DataPoint" +import { lastNonNullPoint } from "../../../helpers/common/serieDataHandling"; + +describe("Obtainment of the last non-null-valued DataPoint from an array", () => { + + let points: IDataPoint[]; + let desiredPoint: IDataPoint; + + it("If the very last point of the array has a non-null value, it is returned", () => { + points = [ + { date: '2014-03-30', value: 2 }, + { date: '2014-11-27', value: 1 }, + { date: '2015-05-07', value: 1 }, + { date: '2017-05-14', value: 3 }, + { date: '2018-03-14', value: 2 }, + { date: '2018-12-09', value: 3 }, + { date: '2019-10-22', value: 2 } + ]; + desiredPoint = lastNonNullPoint(points); + expect(desiredPoint.date).toEqual('2019-10-22'); + expect(desiredPoint.value).toBe(2); + }); + it("The non-null-valued point of the array with the greatest index will be returned", () => { + points = [ + { date: '1988-10-01', value: null }, + { date: '1988-11-01', value: 0 }, + { date: '1988-12-01', value: 25 }, + { date: '1989-01-01', value: null }, + { date: '1989-02-01', value: null } + ]; + desiredPoint = lastNonNullPoint(points); + expect(desiredPoint.date).toEqual('1988-12-01'); + expect(desiredPoint.value).toBe(25); + }); + it("If every point in the array is null-valued, the first one is returned", () => { + points = [ + { date: '2004-01-01', value: null }, + { date: '2005-01-01', value: null }, + { date: '2006-01-01', value: null } + ]; + desiredPoint = lastNonNullPoint(points); + expect(desiredPoint.date).toEqual('2004-01-01'); + expect(desiredPoint.value).toBeNull(); + }); + +}) \ No newline at end of file