Creates an stock price chart widget

This commit is contained in:
Alicia Sykes 2021-12-12 16:30:07 +00:00
parent a77cb9430f
commit 51b7e639cc
4 changed files with 205 additions and 0 deletions

View File

@ -236,6 +236,31 @@ Display current FX rates in your native currency
- KPW - 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 ## Dynamic Widgets

View File

@ -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>

View File

@ -18,6 +18,7 @@
<CryptoWatchList v-else-if="widgetType === 'crypto-watch-list'" :options="widgetOptions" /> <CryptoWatchList v-else-if="widgetType === 'crypto-watch-list'" :options="widgetOptions" />
<XkcdComic v-else-if="widgetType === 'xkcd-comic'" :options="widgetOptions" /> <XkcdComic v-else-if="widgetType === 'xkcd-comic'" :options="widgetOptions" />
<ExchangeRates v-else-if="widgetType === 'exchange-rates'" :options="widgetOptions" /> <ExchangeRates v-else-if="widgetType === 'exchange-rates'" :options="widgetOptions" />
<StockPriceChart v-else-if="widgetType === 'stock-price-chart'" :options="widgetOptions" />
</Collapsable> </Collapsable>
</div> </div>
</template> </template>
@ -31,6 +32,7 @@ import CryptoPriceChart from '@/components/Widgets/CryptoPriceChart.vue';
import CryptoWatchList from '@/components/Widgets/CryptoWatchList.vue'; import CryptoWatchList from '@/components/Widgets/CryptoWatchList.vue';
import XkcdComic from '@/components/Widgets/XkcdComic.vue'; import XkcdComic from '@/components/Widgets/XkcdComic.vue';
import ExchangeRates from '@/components/Widgets/ExchangeRates.vue'; import ExchangeRates from '@/components/Widgets/ExchangeRates.vue';
import StockPriceChart from '@/components/Widgets/StockPriceChart.vue';
import Collapsable from '@/components/LinkItems/Collapsable.vue'; import Collapsable from '@/components/LinkItems/Collapsable.vue';
export default { export default {
@ -45,6 +47,7 @@ export default {
CryptoWatchList, CryptoWatchList,
XkcdComic, XkcdComic,
ExchangeRates, ExchangeRates,
StockPriceChart,
}, },
props: { props: {
widget: Object, widget: Object,

View File

@ -212,6 +212,7 @@ module.exports = {
cryptoWatchList: 'https://api.coingecko.com/api/v3/coins/markets/', cryptoWatchList: 'https://api.coingecko.com/api/v3/coins/markets/',
xkcdComic: 'https://xkcd.vercel.app/', xkcdComic: 'https://xkcd.vercel.app/',
exchangeRates: 'https://v6.exchangerate-api.com/v6/', exchangeRates: 'https://v6.exchangerate-api.com/v6/',
stockPriceChart: 'https://www.alphavantage.co/query',
}, },
/* URLs for web search engines */ /* URLs for web search engines */
searchEngineUrls: { searchEngineUrls: {