diff --git a/demos/rna-news/demo.html b/demos/rna-news/demo.html new file mode 100644 index 0000000..46f4cd7 --- /dev/null +++ b/demos/rna-news/demo.html @@ -0,0 +1,14 @@ + + + + + + + + RNA News Demo - Morningstar Connectors Demos + + +
+ + + diff --git a/demos/rna-news/demo.js b/demos/rna-news/demo.js new file mode 100644 index 0000000..37a38b3 --- /dev/null +++ b/demos/rna-news/demo.js @@ -0,0 +1,141 @@ +const board = Dashboards.board('container', { + dataPool: { + connectors: [{ + id: 'rna', + type: 'MorningstarRNANews', + options: { + security: { + id: 'US0378331005' + }, + postman: { + environmentURL: '/tmp/Environment.json' + } + } + }, + { + id: 'rna-type-amount', + type: 'JSON', + options: { + columnNames: ['Type', 'Amount'], + data: [ + [], + [] + ], + orientation: 'columns', + firstRowAsNames: false + } + } + ] + }, + components: [ + { + renderTo: 'dashboard-col-0', + connector: { + id: 'rna' + }, + type: 'DataGrid', + title: 'News', + dataGridOptions: { + editable: false, + columns: { + Day: { + cellFormatter: function () { + return new Date(this.value) + .toISOString() + .substring(0, 10); + } + } + } + } + }, + { + renderTo: 'dashboard-col-1', + connector: { + id: 'rna-type-amount', + columnAssignment: [{ + seriesId: 'number-per-type', + data: ['Type', 'Amount'] + }] + }, + type: 'Highcharts', + chartOptions: { + chart: { + animation: false, + type: 'column' + }, + title: { + text: 'Number of items per type' + }, + subtitle: { + text: 'Shows number of news items of each kind' + }, + series: [{ + id: 'number-per-type', + name: 'Number per type' + }], + tooltip: { + shared: true, + split: true, + stickOnContact: true + }, + lang: { + accessibility: { + chartContainerLabel: + 'Shows number of news items of each kind' + } + }, + xAxis: { + type: 'category', + accessibility: { + description: 'Kind of news annoucement' + } + }, + yAxis: { + title: { + text: 'Number of announcements' + } + } + } + } + ] +}); + +board.dataPool.getConnectorTable('rna') + .then(async table => { + const types = table.getColumn('Type'); + const uniqueTypes = Array.from(new Set(types)); + const numberPerType = uniqueTypes.map(type => + types.reduce((previous, current) => { + if (current === type) { + return previous + 1; + } + return previous; + }, 0) + ); + + board.dataPool.setConnectorOptions({ + id: 'rna-type-amount', + type: 'JSON', + options: { + columnNames: ['Type', 'Amount'], + orientation: 'columns', + firstRowAsNames: false, + data: [ + uniqueTypes, + numberPerType + ] + } + }); + + // Refresh the component after updating the table + await board.getComponentByCellId('dashboard-col-1').initConnectors(); + await board.getComponentByCellId('dashboard-col-1').update({ + connector: { + id: 'rna-type-amount', + columnAssignment: [{ + seriesId: 'number-per-type', + data: ['Type', 'Amount'] + }] + } + }); + }); diff --git a/src/RNANews/README.md b/src/RNANews/README.md new file mode 100644 index 0000000..9eec58f --- /dev/null +++ b/src/RNANews/README.md @@ -0,0 +1,8 @@ +Morningstar RNA News Connector +=================================================== + +This Highcharts Dashboards connector provides access to the [Morningstar RNANews API]. The JSON from the API is converted into a DataTable. + + + +[Morningstar RNANews API]: https://developer.morningstar.com/direct-web-services/documentation/api-reference/time-series/regulatory-news-announcements \ No newline at end of file diff --git a/src/RNANews/RNANewsConnector.ts b/src/RNANews/RNANewsConnector.ts new file mode 100644 index 0000000..a184f54 --- /dev/null +++ b/src/RNANews/RNANewsConnector.ts @@ -0,0 +1,202 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Eskil Gjerde Sviggum + * + * */ + + +'use strict'; + + +/* * + * + * Imports + * + * */ + +import External from '../Shared/External'; +import MorningstarConnector from '../Shared/MorningstarConnector'; +import MorningstarAPI from '../Shared/MorningstarAPI'; +import MorningstarURL from '../Shared/MorningstarURL'; +import RNANewsOptions from './RNANewsOptions'; +import RNANewsConverter from './RNANewsConverter'; +import RNANewsJSON from './RNANewsJSON'; + +/* * + * + * Class + * + * */ + +class RNANewsConnector extends MorningstarConnector { + + /** + * Constructs an instance of RNANewsConnector. + * @param {RNANewsOptions} [options] + * Options for the connector and converter. + */ + public constructor( + options: RNANewsOptions + ) { + super(options); + + this.converter = new RNANewsConverter(options); + this.options = options; + } + + /* * + * + * Properties + * + * */ + + + public override readonly converter: RNANewsConverter; + + + public override readonly options: RNANewsOptions; + + /* * + * + * Functions + * + * */ + + /** + * Loads RNANews data from Morningstar. + * + * @return {Promise} + * Same connector instance with modified table. + */ + public override async load(): Promise { + const options = this.options; + const { + security, + startDate, + endDate, + maxStories, + localization + } = options; + + if (!security) { + return this; + } + + const url = new MorningstarURL('timeseries/rna-news'); + const searchParams = url.searchParams; + const api = new MorningstarAPI(options.api); + + searchParams.setSecurityOptions([security]); + + if (startDate) { + const date = RNANewsConnector.validateAndFormatDate(startDate); + searchParams.set('startDate', date); + } + + if (endDate) { + const date = RNANewsConnector.validateAndFormatDate(endDate); + searchParams.set('endDate', date); + } + + if (maxStories) { + const numericMaxStories = Number(maxStories); + if (!Number.isInteger(numericMaxStories)) { + throw new Error(`Expected maxStories to be integer, but is instead ${maxStories}`); + } + searchParams.set('maxStories', '' + maxStories); + } + + if (localization) { + searchParams.setLocalizationOptions(localization); + } + + const response = await api.fetch(url); + const json = await response.json() as RNANewsJSON.Response; + + this.converter.parse({ json }); + + this.table.deleteColumns(); + this.table.setColumns(this.converter.getTable().getColumns()); + + return this; + } + +} + +/* * + * + * Class Namespace + * + * */ + +namespace RNANewsConnector { + /** + * If a number is provided, it is treated as a unix timestamp in + * milliseconds and converted to format `yyyy-MM-dd`. + * If a string is provided, it is validated to conform to the format. + * @private + * @param {number | string} date date as a timestamp of formatted string + * @return {string} date formatted as `yyyy-MM-dd`. + */ + export function validateAndFormatDate(date: number | string): string { + let timestamp: number; + if (typeof date === 'string') { + // Check if string is a number, likely a timestamp + if (!Number.isNaN(Number(date))) { + timestamp = Number(date); + } else { + const parsedDate = Date.parse(date); + if (Number.isNaN(parsedDate)) { + throw new Error(`The date ${date} is not a valid date.`); + } + timestamp = parsedDate; + } + } else if (typeof date === 'number') { + timestamp = date; + } else { + throw new Error( + 'The provided date is not of type string, nor number.' + ); + } + + return new Date(timestamp) + .toISOString() + .substring(0, 10); + } +} + +/* * + * + * Registry + * + * */ + + +declare module '@highcharts/dashboards/es-modules/Data/Connectors/DataConnectorType' { + interface DataConnectorTypes { + MorningstarRNANews: typeof RNANewsConnector; + } +} + + +External.DataConnector.registerType( + 'MorningstarRNANews', + RNANewsConnector +); + + +/* * + * + * Default Export + * + * */ + + +export default RNANewsConnector; diff --git a/src/RNANews/RNANewsConverter.ts b/src/RNANews/RNANewsConverter.ts new file mode 100644 index 0000000..f0e4ea9 --- /dev/null +++ b/src/RNANews/RNANewsConverter.ts @@ -0,0 +1,152 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Eskil Gjerde Sviggum + * + * */ + +'use strict'; + +import * as External from '../Shared/External'; +/* * + * + * Imports + * + * */ + +import MorningstarConverter from '../Shared/MorningstarConverter'; +import RNANewsJSON from './RNANewsJSON'; +import { RNANewsConverterOptions } from './RNANewsOptions'; + +/* * + * + * Class + * + * */ + +/** + * Handles parsing and transformation of + * Regulatory News Announcements to a table. + * + * @private + */ +class RNANewsConverter extends MorningstarConverter { + + /* * + * + * Constructor + * + * */ + + /** + * Constructs an instance of the RNANewsConverter. + * + * @param {RNANewsConverterOptions} [options] + * Options for the converter. + */ + constructor( + options?: RNANewsConverterOptions + ) { + super(options); + + this.columns = []; + this.header = []; + this.options = options as Required; + } + + /* * + * + * Properties + * + * */ + + private columns: (string | number)[][]; + private header: string[]; + + /** + * Options for the DataConverter. + */ + public override readonly options: Required; + + /* * + * + * Functions + * + * */ + + /** + * Initiates the parsing of the RNANews + * + * @param {RNANewsConverterOptions}[options] + * Options for the parser + * + */ + public parse( + options: RNANewsConverterOptions, + ): (boolean|undefined) { + + this.header = ['Day', 'Title', 'Source', 'Type']; + this.columns = []; + + if (!options.json) { + return false; + } + + // Validate JSON + + if (!RNANewsJSON.isResponse(options.json)) { + throw new Error('Invalid data'); + } + + // Transform rows + const rows: RNANewsJSON.RNANewsItem[] = []; + for (const dailyNews of options.json) { + const day = Number(dailyNews.day); + const rowsForDay = dailyNews.items.map( + (newsItem): RNANewsJSON.RNANewsItem => { + const [, title, source, type] = newsItem; + return [day, title, source, type]; + }); + rows.push(...rowsForDay); + } + + // Transpose rows into columns. + const columns: Array[] = []; + for (let column = 0; column < this.header.length; column++) { + const columnFields: Array = []; + for (let row = 0; row < rows.length; row++) { + columnFields.push(rows[row][column]); + } + columns.push(columnFields); + } + + this.columns = columns; + + return true; + } + + /** + * Handles converting the parsed data to a table. + * + * @return {DataTable} + * Table from the parsed RNANews + */ + public override getTable(): External.DataTable { + return MorningstarConverter.getTableFromColumns(this.columns, this.header); + } + +} + +/* * + * + * Default Export + * + * */ + +export default RNANewsConverter; diff --git a/src/RNANews/RNANewsJSON.ts b/src/RNANews/RNANewsJSON.ts new file mode 100644 index 0000000..b76b8dd --- /dev/null +++ b/src/RNANews/RNANewsJSON.ts @@ -0,0 +1,112 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Eskil Gjerde Sviggum + * + * */ + + +'use strict'; + + +/* * + * + * Namespace + * + * */ + +namespace RNANewsJSON { + + + /* * + * + * Declarations + * + * */ + + export type RNANewsResponseItem = [id: string, title: string, source: string, type: string]; + + export type RNANewsItem = [day: number, title: string, source: string, type: string]; + + /** + * The response JSON for RNANews from Morningstar. + */ + export interface ResponseItem { + /** + * A UNIX timestamp in milliseconds specifying the day + * at which the news items were published. + */ + day: string; + + /** + * List of news items. + * + * A news-item is a 4-tuple with the following fields: + * - Unique identifier + * - Title of announcement. + * - Source + * - Type + */ + items: RNANewsResponseItem[]; + } + + export type Response = ResponseItem[]; + + /* * + * + * Functions + * + * */ + + export function isResponse( + json?: unknown + ): json is Response { + return ( + !!json && + Array.isArray(json) && + json.length === 0 || + isResponseItem((json as Array)[0]) + ); + } + + + function isResponseItem( + json?: unknown + ): json is ResponseItem { + return ( + !!json && + typeof json === 'object' && + typeof (json as ResponseItem).day === 'string' && + typeof (json as ResponseItem).items === 'object' && + isRNANewsResponseItem(typeof (json as ResponseItem).items[0]) + ); + } + + function isRNANewsResponseItem( + json?: unknown + ): json is RNANewsResponseItem { + return ( + !!json && + Array.isArray(json) && + json.length === 4 && + !json.find(entry => typeof entry !== 'string') + ); + } + +} + + +/* * +* +* Default Export +* +* */ + + +export default RNANewsJSON; diff --git a/src/RNANews/RNANewsOptions.ts b/src/RNANews/RNANewsOptions.ts new file mode 100644 index 0000000..da660c4 --- /dev/null +++ b/src/RNANews/RNANewsOptions.ts @@ -0,0 +1,67 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Eskil Gjerde Sviggum + * + * */ + + +/* * + * + * Imports + * + * */ +import type LocalizationOptions from '../Shared/LocalizationOptions'; +import MorningstarOptions, { MorningstarConverterOptions, MorningstarSecurityOptions } from "../Shared/MorningstarOptions"; +import RNANewsJSON from './RNANewsJSON'; + +export interface RNANewsOptions extends RNANewsConverterOptions, MorningstarOptions { + + /** + * Security to retrieve. + */ + security?: MorningstarSecurityOptions; + + /** + * The start date of the time series. + * Should be either a UNIX timestamp, + * or a string formatted as `yyyy-MM-dd`. + */ + startDate?: number | string; + + /** + * The end date of the time series. + * Should be either a UNIX timestamp, + * or a string formatted as `yyyy-MM-dd`. + */ + endDate?: number | string; + + /** + * The maximum number of announcements to load. + */ + maxStories?: number; + + /** + * Localization options. + */ + localization?: LocalizationOptions; + +} + +export interface RNANewsConverterOptions extends MorningstarConverterOptions { + json?: RNANewsJSON.Response +} + +/* * + * + * Default Export + * + * */ + +export default RNANewsOptions; diff --git a/src/RNANews/index.ts b/src/RNANews/index.ts new file mode 100644 index 0000000..801111b --- /dev/null +++ b/src/RNANews/index.ts @@ -0,0 +1,50 @@ +/* * + * + * (c) 2009-2024 Highsoft AS + * + * License: www.highcharts.com/license + * + * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! + * + * Authors: + * - Eskil Gjerde Sviggum + * + * */ + + +'use strict'; + + +/* * + * + * Imports + * + * */ + + +import RNANewsConverter from "./RNANewsConverter"; +import RNANewsConnector from "./RNANewsConnector"; + + +/* * + * + * Exports + * + * */ + + +export * from './RNANewsConverter'; +export * from './RNANewsConnector'; + + +/* * + * + * Default Export + * + * */ + + +export default { + RNANewsConverter, + RNANewsConnector +}; diff --git a/src/Shared/LocalizationOptions.ts b/src/Shared/LocalizationOptions.ts index d31d503..31be8be 100644 --- a/src/Shared/LocalizationOptions.ts +++ b/src/Shared/LocalizationOptions.ts @@ -38,10 +38,21 @@ export interface DateOptions { export interface LocalizationOptions { + /** + * A two-letter ISO-3166-1 alpha-2 country code. + * For example: US, JP + */ country: string; + /** + * The currency to use. + */ currency: Currency; + /** + * A two-letter ISO-639-1 alpha-2 language code. + * For example: en, ja + */ language: string; } diff --git a/src/Shared/MorningstarSearchParams.ts b/src/Shared/MorningstarSearchParams.ts index d6c450f..86c3894 100644 --- a/src/Shared/MorningstarSearchParams.ts +++ b/src/Shared/MorningstarSearchParams.ts @@ -23,6 +23,7 @@ import type { MorningstarSecurityOptions } from './MorningstarOptions'; +import LocalizationOptions from './LocalizationOptions'; /* * @@ -70,6 +71,30 @@ export class MorningstarSearchParams extends URLSearchParams { return this; } + /** + * Sets `languageId` based on given localization options. + * + * @param options + * Localization options to set. + * + * @return + * The modified search parameters as reference. + */ + public setLocalizationOptions( + options: LocalizationOptions + ): MorningstarSearchParams { + + const { + country, + language + } = options; + + const languageCultureCode = `${language.toLowerCase()}-${country.toUpperCase()}`; + this.set('languageId', languageCultureCode); + + return this; + } + } diff --git a/src/index.ts b/src/index.ts index ae986d7..55eb171 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ import MorningstarAPI from './Shared/MorningstarAPI'; import TimeSeries from './TimeSeries/index'; +import RNANews from './RNANews/index'; /* * @@ -35,6 +36,7 @@ import TimeSeries from './TimeSeries/index'; export * as Shared from './Shared/index'; export * as TimeSeries from './TimeSeries/index'; +export * as RNANews from './RNANews/index'; /* * @@ -46,5 +48,6 @@ export * as TimeSeries from './TimeSeries/index'; export default { MorningstarAPI, - TimeSeries + TimeSeries, + RNANews };