mirror of
https://github.com/Lissy93/dashy.git
synced 2024-11-27 18:32:55 +03:00
✨ Develops a crypto price chart widget
This commit is contained in:
parent
c12eac2bbf
commit
ebd4c48300
@ -86,6 +86,30 @@ Displays the weather (temperature and conditions) for the next few days for a gi
|
||||
units: imperial
|
||||
```
|
||||
|
||||
### Crypto Token Price History
|
||||
|
||||
Shows recent price history for a given crypto asset, using price data fetched from [CoinGecko](https://www.coingecko.com/)
|
||||
|
||||
##### Options
|
||||
|
||||
**Field** | **Type** | **Required** | **Description**
|
||||
--- | --- | --- | ---
|
||||
**`asset`** | `string` | Required | Name of a crypto asset, coin or token to fetch price data for
|
||||
**`currency`** | `string` | _Optional_ | The fiat currency to display results in, expressed as an ISO-4217 alpha code (see [list of currencies](https://www.iban.com/currency-codes)). Defaults to `USD`
|
||||
**`numDays`** | `number` | _Optional_ | The number of days of price history to render. Defaults to `7`, min: `1`, max: `30` days
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- name: Bitcoin Price
|
||||
icon: fab fa-bitcoin
|
||||
type: crypto-price-chart
|
||||
options:
|
||||
asset: bitcoin
|
||||
currency: GBP
|
||||
numDays: 7
|
||||
```
|
||||
|
||||
### TFL Status
|
||||
|
||||
Shows real-time tube status of the London Underground. All options are optional.
|
||||
@ -101,7 +125,12 @@ Shows real-time tube status of the London Underground. All options are optional.
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
- name: TFL Status
|
||||
- name: London Underground
|
||||
type: tfl-status
|
||||
```
|
||||
|
||||
```yaml
|
||||
- name: Commute
|
||||
icon: '🚋'
|
||||
type: tfl-status
|
||||
options:
|
||||
|
182
src/components/Widgets/CryptoPriceChart.vue
Normal file
182
src/components/Widgets/CryptoPriceChart.vue
Normal file
@ -0,0 +1,182 @@
|
||||
<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 crypto asset to fetch price data for */
|
||||
asset() {
|
||||
const userChoice = this.options.asset;
|
||||
if (typeof userChoice === 'string') return userChoice;
|
||||
return 'bitcoin';
|
||||
},
|
||||
/* Number of days worth of history to fetch and display */
|
||||
numDays() {
|
||||
const userChoice = this.options.numDays;
|
||||
if (!Number.isNaN(userChoice) && userChoice < 30 && userChoice > 0.15) {
|
||||
return userChoice;
|
||||
}
|
||||
return 7;
|
||||
},
|
||||
/* The fiat currency to calculate price data in */
|
||||
currency() {
|
||||
const userChoice = this.options.currency;
|
||||
if (typeof userChoice === 'string') return userChoice;
|
||||
return 'USD';
|
||||
},
|
||||
/* 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;
|
||||
},
|
||||
/* The formatted GET request API endpoint to fetch crypto data from */
|
||||
endpoint() {
|
||||
return `${widgetApiEndpoints.cryptoPrices}${this.asset}/`
|
||||
+ `market_chart?vs_currency=${this.currency}&days=${this.numDays}`;
|
||||
},
|
||||
/* A sudo-random ID for the chart DOM element */
|
||||
chartId() {
|
||||
return `crypto-price-chart-${Math.round(Math.random() * 10000)}`;
|
||||
},
|
||||
getChartColor() {
|
||||
const cssVars = getComputedStyle(document.documentElement);
|
||||
return cssVars.getPropertyValue('--widget-text-color').trim() || '#7cd6fd';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Create new chart, using the crypto data */
|
||||
generateChart() {
|
||||
return new Chart(`#${this.chartId}`, {
|
||||
title: `${this.asset} 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} ${this.currency}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
/* Make GET request to CoinGecko API endpoint */
|
||||
fetchData() {
|
||||
axios.get(this.endpoint)
|
||||
.then((response) => {
|
||||
try {
|
||||
this.lineStatuses = this.processData(response.data);
|
||||
} catch (chartingError) {
|
||||
ErrorHandler('Unable to plot results on chart', chartingError);
|
||||
}
|
||||
})
|
||||
.catch((dataFetchError) => {
|
||||
ErrorHandler('Unable to fetch crypto data', dataFetchError);
|
||||
});
|
||||
},
|
||||
/* Generate price history in a format that can be consumed by the chart
|
||||
* To improve efficiency, only a certain amount of data points are plotted
|
||||
* depending on user preference. An average is then calculated between points
|
||||
*/
|
||||
processData(data) {
|
||||
const priceChartData = [];
|
||||
const priceLabels = [];
|
||||
const interval = Math.round(data.prices.length / this.dataPoints);
|
||||
const showTime = this.numDays < 5;
|
||||
// Counters for calculating averages between data points
|
||||
let tmpCounter = 0; let tmpTotal = 0;
|
||||
const incrementAverage = (add) => {
|
||||
tmpCounter += 1; tmpTotal += add;
|
||||
if (add === null) { tmpCounter = 0; tmpTotal = 0; }
|
||||
};
|
||||
// For each data point, calc average, and if interval is right, then append
|
||||
data.prices.forEach((priceGroup, index) => {
|
||||
incrementAverage(priceGroup[1]); // Increment averages
|
||||
if (index % interval === 0) {
|
||||
const price = this.formatPrice(tmpTotal / tmpCounter);
|
||||
priceLabels.push(this.formatDate(priceGroup[0], showTime));
|
||||
priceChartData.push(price);
|
||||
incrementAverage(null); // Reset counter
|
||||
}
|
||||
});
|
||||
// Combine results with chart config
|
||||
this.chartData = {
|
||||
labels: priceLabels,
|
||||
datasets: [
|
||||
{
|
||||
name: 'Price',
|
||||
type: 'bar',
|
||||
values: priceChartData,
|
||||
},
|
||||
],
|
||||
};
|
||||
// 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, includeTime) {
|
||||
const localFormat = navigator.language;
|
||||
const dateFormat = { weekday: 'short', day: 'numeric', month: 'short' };
|
||||
const timeFormat = { hour: 'numeric', minute: 'numeric', second: 'numeric' };
|
||||
const date = new Date(timestamp).toLocaleDateString(localFormat, dateFormat);
|
||||
const time = Intl.DateTimeFormat(localFormat, timeFormat).format(timestamp);
|
||||
return `${date} ${includeTime ? time : ''}`;
|
||||
},
|
||||
/* Format the price, rounding to given number of decimal places */
|
||||
formatPrice(price) {
|
||||
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>
|
@ -14,6 +14,7 @@
|
||||
<Weather v-else-if="widgetType === 'weather'" :options="widgetOptions" />
|
||||
<WeatherForecast v-else-if="widgetType === 'weather-forecast'" :options="widgetOptions" />
|
||||
<TflStatus v-else-if="widgetType === 'tfl-status'" :options="widgetOptions" />
|
||||
<CryptoPriceChart v-else-if="widgetType === 'crypto-price-chart'" :options="widgetOptions" />
|
||||
</Collapsable>
|
||||
</div>
|
||||
</template>
|
||||
@ -23,6 +24,7 @@ import Clock from '@/components/Widgets/Clock.vue';
|
||||
import Weather from '@/components/Widgets/Weather.vue';
|
||||
import WeatherForecast from '@/components/Widgets/WeatherForecast.vue';
|
||||
import TflStatus from '@/components/Widgets/TflStatus.vue';
|
||||
import CryptoPriceChart from '@/components/Widgets/CryptoPriceChart.vue';
|
||||
import Collapsable from '@/components/LinkItems/Collapsable.vue';
|
||||
|
||||
export default {
|
||||
@ -33,6 +35,7 @@ export default {
|
||||
Weather,
|
||||
WeatherForecast,
|
||||
TflStatus,
|
||||
CryptoPriceChart,
|
||||
},
|
||||
props: {
|
||||
widget: Object,
|
||||
|
Loading…
Reference in New Issue
Block a user