From 51b7e639cc30e2355354ee1d8607c4e68058b719 Mon Sep 17 00:00:00 2001 From: Alicia Sykes <sykes.alicia@gmail.com> Date: Sun, 12 Dec 2021 16:30:07 +0000 Subject: [PATCH] :sparkles: Creates an stock price chart widget --- docs/widgets.md | 25 +++ src/components/Widgets/StockPriceChart.vue | 176 +++++++++++++++++++++ src/components/Widgets/WidgetBase.vue | 3 + src/utils/defaults.js | 1 + 4 files changed, 205 insertions(+) create mode 100644 src/components/Widgets/StockPriceChart.vue diff --git a/docs/widgets.md b/docs/widgets.md index 768d6a29..109bdf2a 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -236,6 +236,31 @@ Display current FX rates in your native currency - KPW ``` +### Stock Price History + +Shows recent price history for a given publicly-traded stock or share + +<p align="center"><img width="400" src="https://i.ibb.co/XZHRb4f/stock-price.png" /></p> + +##### Options + +**Field** | **Type** | **Required** | **Description** +--- | --- | --- | --- +**`apiKey`** | `string` | Required | API key for [Alpha Vantage](https://www.alphavantage.co/), you can get a free API key [here](https://www.alphavantage.co/support/#api-key) +**`stock`** | `string` | Required | The stock symbol for the asset to fetch data for +**`priceTime`** | `string` | _Optional_ | The time to fetch price for. Can be `high`, `low`, `open` or `close`. Defaults to `high` + +##### Example + +```yaml +- name: CloudFlare Stock Price + icon: fas fa-analytics + type: stock-price-chart + options: + stock: NET + apiKey: PGUWSWD6CZTXMT8N +``` + --- ## Dynamic Widgets diff --git a/src/components/Widgets/StockPriceChart.vue b/src/components/Widgets/StockPriceChart.vue new file mode 100644 index 00000000..c4dcbf94 --- /dev/null +++ b/src/components/Widgets/StockPriceChart.vue @@ -0,0 +1,176 @@ +<template> +<div class="crypto-price-chart" :id="chartId"></div> +</template> + +<script> +import { Chart } from 'frappe-charts/dist/frappe-charts.min.esm'; +import axios from 'axios'; +import WidgetMixin from '@/mixins/WidgetMixin'; +import ErrorHandler from '@/utils/ErrorHandler'; +import { widgetApiEndpoints } from '@/utils/defaults'; + +export default { + mixins: [WidgetMixin], + components: {}, + data() { + return { + chartData: null, + chartDom: null, + }; + }, + mounted() { + this.fetchData(); + }, + computed: { + /* The stock or share asset symbol to fetch data for */ + stock() { + return this.options.stock; + }, + /* The time interval between data points, in minutes */ + interval() { + return `${(this.options.interval || 30)}min`; + }, + /* The users API key for AlphaVantage */ + apiKey() { + return this.options.apiKey; + }, + /* The formatted GET request API endpoint to fetch stock data from */ + endpoint() { + const func = 'TIME_SERIES_INTRADAY'; + return `${widgetApiEndpoints.stockPriceChart}?function=${func}` + + `&symbol=${this.stock}&interval=${this.interval}&apikey=${this.apiKey}`; + }, + /* The number of data points to render on the chart */ + dataPoints() { + const userChoice = this.options.dataPoints; + if (!Number.isNaN(userChoice) && userChoice < 100 && userChoice > 5) { + return userChoice; + } + return 30; + }, + /* A sudo-random ID for the chart DOM element */ + chartId() { + return `stock-price-chart-${Math.round(Math.random() * 10000)}`; + }, + /* Get color hex code for chart, from CSS variable, or user choice */ + getChartColor() { + if (this.options.chartColor) return this.options.chartColor; + const cssVars = getComputedStyle(document.documentElement); + return cssVars.getPropertyValue('--widget-text-color').trim() || '#7cd6fd'; + }, + /* Which price for each interval should be used (API requires in stupid format) */ + priceTime() { + const usersChoice = this.options.priceTime || 'high'; + switch (usersChoice) { + case ('open'): return '1. open'; + case ('high'): return '2. high'; + case ('low'): return '3. low'; + case ('close'): return '4. close'; + case ('volume'): return '5. volume'; + default: return '2. high'; + } + }, + }, + methods: { + /* Create new chart, using the crypto data */ + generateChart() { + return new Chart(`#${this.chartId}`, { + title: `${this.stock} Price Chart`, + data: this.chartData, + type: 'axis-mixed', + height: 200, + colors: [this.getChartColor, '#743ee2'], + truncateLegends: true, + lineOptions: { + regionFill: 1, + hideDots: 1, + }, + axisOptions: { + xIsSeries: true, + xAxisMode: 'tick', + }, + tooltipOptions: { + formatTooltipY: d => `$${d}`, + }, + }); + }, + /* Make GET request to CoinGecko API endpoint */ + fetchData() { + axios.get(this.endpoint) + .then((response) => { + if (response.data.note) { + ErrorHandler('API Error', response.data.Note); + } else if (response.data['Error Message']) { + ErrorHandler('API Error', response.data['Error Message']); + } else { + this.processData(response.data); + } + }) + .catch((error) => { + ErrorHandler('Unable to fetch stock price data', error); + }); + }, + /* Convert data returned by API into a format that can be consumed by the chart + * To improve efficiency, only a certain amount of data points are plotted + */ + processData(data) { + const priceLabels = []; + const priceValues = []; + const dataKey = `Time Series (${this.interval})`; + const rawMarketData = data[dataKey]; + const interval = Math.round(Object.keys(rawMarketData).length / this.dataPoints); + Object.keys(rawMarketData).forEach((timeGroup, index) => { + if (index % interval === 0) { + priceLabels.push(this.formatDate(timeGroup)); + priceValues.push(this.formatPrice(rawMarketData[timeGroup][this.priceTime])); + } + }); + // // Combine results with chart config + this.chartData = { + labels: priceLabels, + datasets: [ + { name: `Price ${this.priceTime}`, type: 'bar', values: priceValues }, + ], + }; + // // Call chart render function + this.renderChart(); + }, + /* Uses class data to render the line chart */ + renderChart() { + this.chartDom = this.generateChart(); + }, + /* Format the date for a given time stamp, also include time if required */ + formatDate(timestamp) { + const localFormat = navigator.language; + const dateFormat = { weekday: 'short', day: 'numeric', month: 'short' }; + const date = new Date(timestamp).toLocaleDateString(localFormat, dateFormat); + return date; + }, + /* Format the price, rounding to given number of decimal places */ + formatPrice(priceStr) { + const price = parseInt(priceStr, 10); + let numDecimals = 0; + if (price < 10) numDecimals = 1; + if (price < 1) numDecimals = 2; + if (price < 0.1) numDecimals = 3; + if (price < 0.01) numDecimals = 4; + if (price < 0.001) numDecimals = 5; + return price.toFixed(numDecimals); + }, + }, +}; +</script> + +<style lang="scss"> +.crypto-price-chart .chart-container { + text.title { + text-transform: capitalize; + color: var(--widget-text-color); + } + .axis, .chart-label { + fill: var(--widget-text-color); + opacity: var(--dimming-factor); + &:hover { opacity: 1; } + } +} +</style> diff --git a/src/components/Widgets/WidgetBase.vue b/src/components/Widgets/WidgetBase.vue index 61275373..ba78aae4 100644 --- a/src/components/Widgets/WidgetBase.vue +++ b/src/components/Widgets/WidgetBase.vue @@ -18,6 +18,7 @@ <CryptoWatchList v-else-if="widgetType === 'crypto-watch-list'" :options="widgetOptions" /> <XkcdComic v-else-if="widgetType === 'xkcd-comic'" :options="widgetOptions" /> <ExchangeRates v-else-if="widgetType === 'exchange-rates'" :options="widgetOptions" /> + <StockPriceChart v-else-if="widgetType === 'stock-price-chart'" :options="widgetOptions" /> </Collapsable> </div> </template> @@ -31,6 +32,7 @@ import CryptoPriceChart from '@/components/Widgets/CryptoPriceChart.vue'; import CryptoWatchList from '@/components/Widgets/CryptoWatchList.vue'; import XkcdComic from '@/components/Widgets/XkcdComic.vue'; import ExchangeRates from '@/components/Widgets/ExchangeRates.vue'; +import StockPriceChart from '@/components/Widgets/StockPriceChart.vue'; import Collapsable from '@/components/LinkItems/Collapsable.vue'; export default { @@ -45,6 +47,7 @@ export default { CryptoWatchList, XkcdComic, ExchangeRates, + StockPriceChart, }, props: { widget: Object, diff --git a/src/utils/defaults.js b/src/utils/defaults.js index c2273693..8f978675 100644 --- a/src/utils/defaults.js +++ b/src/utils/defaults.js @@ -212,6 +212,7 @@ module.exports = { cryptoWatchList: 'https://api.coingecko.com/api/v3/coins/markets/', xkcdComic: 'https://xkcd.vercel.app/', exchangeRates: 'https://v6.exchangerate-api.com/v6/', + stockPriceChart: 'https://www.alphavantage.co/query', }, /* URLs for web search engines */ searchEngineUrls: {