diff --git a/packages/stock-charts/src/app.js b/packages/stock-charts/src/app.js index 0f1f06a..d78bc85 100644 --- a/packages/stock-charts/src/app.js +++ b/packages/stock-charts/src/app.js @@ -23,7 +23,6 @@ class App extends React.Component { }); }); - this.state = { chartPeriod: ChartPeriod.m12, chartColor: null @@ -69,13 +68,13 @@ class App extends React.Component { switch(this.state.chartName) { case 'hloc': return this.changeChartPeriod(p)} + onInitialize={(p, c) => this.handleChartInitialize(p, c)} onColorChange={(c) => this.changeChartColor(c)} storageKey={Config.HLOC_STGKEY} />; case 'trendline': return this.changeChartPeriod(p)} + onInitialize={(p, c) => this.handleChartInitialize(p, c)} onColorChange={(c) => this.changeChartColor(c)} storageKey={Config.TRENDLINE_STGKEY} />; @@ -85,9 +84,8 @@ class App extends React.Component { } render() { - const chartColor = this.state.chartColor const headerStyle = { - boxShadow: chartColor ? '0px 0px 20px ' + chartColor : 'none' + backgroundColor: this.state.chartColor }; return (
@@ -119,6 +117,13 @@ class App extends React.Component { return this.state.chartPeriod === value; } + handleChartInitialize(period, color) { + this.setState({ + chartPeriod: period, + chartColor: color + }); + } + changeChartPeriod(value) { this.setState({ chartPeriod: value diff --git a/packages/stock-charts/src/chartController.js b/packages/stock-charts/src/chartController.js new file mode 100644 index 0000000..c7d3d2e --- /dev/null +++ b/packages/stock-charts/src/chartController.js @@ -0,0 +1,137 @@ +import { ChannelName, ChannelTopics } from 'stock-core'; + +const fin = window.fin; + +export class ChartController { + + _channelPromise; + + /** + * Indicates that disconnection occured and currently connection is in progress. + * It is used to avoid multiple connections in such cases. + */ + _reconnecting = false; + + constructor(props) { + this.props = props; + this.currentSymbol = null; + + this._initChannel(); + } + + _initChannel() { + if (typeof fin === 'undefined') { + console.warn('Channels cannot be initialized because "fin" is undefined.'); + return; + } + + this._channelPromise = fin.InterApplicationBus.Channel.connect(ChannelName) + .then(channel => this._handleConnect(channel)); + } + + _handleConnect(channel) { + this._reconnecting = false; + console.log('Channels: connected'); + this._loadItems(channel); + channel.register(ChannelTopics.CurrentChanged, symbol => this._currentChanged(symbol)); + channel.register(ChannelTopics.ItemAdded, item => this._itemAdded(item)); + channel.register(ChannelTopics.ItemRemoved, item => this._itemRemoved(item)); + channel.register(ChannelTopics.ItemChanged, item => { /* do nothing */ }); + channel.onDisconnection(channelInfo => this._handleDisconnect(channelInfo)); + return channel; + } + + _handleDisconnect(channelInfo) { + if (this._reconnecting) { + return; + } + this._reconnecting = true; + // handle the channel lifecycle here - we can connect again which will return a promise + // that will resolve if/when the channel is re-created. + console.log('Channels: disconnected'); + this._initChannel(); + } + + _loadItems(channel) { + channel.dispatch(ChannelTopics.LoadItems).then(response => { + const current = response.current; + const items = response.items; + console.log('Channels: received response "loadItems". Items: ' + items.length); + items.forEach(item => { + let prices = this.props.parsePrices(item.prices); + this.props.portfolio.addItem(item.symbol, item.chart, item.name, item.color, prices); + }); + window.setTimeout(() => { + this._currentChanged(current); + }); + }); + + console.log('Channels: sent message "loadItems"'); + } + + _itemAdded(item) { + console.log('Channels: received message "itemAdded"'); + + let prices = this.props.parsePrices(item.prices); + this.props.portfolio.addItem(item.symbol, item.chart, item.name, item.color, prices); + } + + _itemRemoved(item) { + console.log('Channels: received message "itemRemoved"'); + + this.props.portfolio.removeItem(item.symbol); + } + + // update chart selection to match portfolio selection + _currentChanged(symbol) { + console.log('Channels: received message "currentChanged"'); + + const chartElement = this.props.chartRef.current; + if (!chartElement) { + return; + } + + if (!this.currentSymbol) { + this.currentSymbol = symbol; + this.changeCurrent(this.currentSymbol); + return; + } + + /* eslint-disable-next-line eqeqeq */ + if (symbol == this.currentSymbol) { + return; + } + + this.currentSymbol = symbol; + + chartElement.classList.remove('fadein', 'fadeout'); + chartElement.classList.add('fadein'); + } + + handleAnimationStart() { + // do nothing here + } + + handleAnimationEnd() { + const chartElement = this.props.chartRef.current; + if (chartElement.classList.contains('fadeout')) { + + chartElement.classList.remove('fadeout'); + } else if (chartElement.classList.contains('fadein')) { + + chartElement.classList.remove('fadein'); + chartElement.style.visibility = 'hidden'; + + this.changeCurrent(this.currentSymbol); + + chartElement.style.visibility = 'visible'; + chartElement.classList.add('fadeout'); + } + } + + changeCurrent(symbol) { + /* eslint-disable-next-line eqeqeq */ + const current = this.props.portfolio.view.items.find(pi => pi.symbol == symbol); + this.props.handleCurrentChange(current); + } +} \ No newline at end of file diff --git a/packages/stock-charts/src/hloc-chart/chart.js b/packages/stock-charts/src/hloc-chart/chart.js index 3964537..de1aaa1 100644 --- a/packages/stock-charts/src/hloc-chart/chart.js +++ b/packages/stock-charts/src/hloc-chart/chart.js @@ -1,38 +1,34 @@ import React from 'react'; import * as wjChart from "@grapecity/wijmo.react.chart"; import * as wjChartAnalysis from "@grapecity/wijmo.react.chart.analytics"; -import { ChannelName, ChannelTopics, Portfolio } from 'stock-core'; +import { Portfolio } from 'stock-core'; +import { ChartController } from '../chartController'; import './chart.css'; -const fin = window.fin; - class Chart extends React.Component { - _portfolio; - _channelPromise; - - /** - * Indicates that disconnection occured and currently connection is in progress. - * It is used to avoid multiple connections in such cases. - */ - _reconnecting = false; - constructor(props) { super(props); - this.state = { - current: null - }; - + this.chartRef = React.createRef(); + // create portfolio - this._portfolio = new Portfolio({ + this.portfolio = new Portfolio({ storageKey: this.props.storageKey, - mapToChartData: this._mapToChartData + mapToChartData: this.mapToChartData + }); + + this.controller = new ChartController({ + chartRef: this.chartRef, + portfolio: this.portfolio, + parsePrices: this.parsePrices, + handleCurrentChange: this.handleCurrentChange.bind(this) }); - this.props.onPeriodChange(this._portfolio.chartPeriod); - this.props.onColorChange(null); + this.state = { + current: null + }; - this._initChannel(); + this.props.onInitialize(this.portfolio.chartPeriod, null); } renderChartContent() { @@ -60,51 +56,22 @@ class Chart extends React.Component { } render() { - this._portfolio.chartPeriod = this.props.period; + this.portfolio.chartPeriod = this.props.period; return ( - - {this.renderChartContent()} - - - - +
+ + {this.renderChartContent()} + + + + +
); } - _initChannel() { - if (typeof fin === 'undefined') { - console.warn('Channels cannot be initialized because "fin" is undefined.'); - return; - } - - this._channelPromise = fin.InterApplicationBus.Channel.connect(ChannelName) - .then(channel => this._handleConnect(channel)); - } - - _handleConnect(channel) { - this._reconnecting = false; - console.log('Channels: connected'); - this._loadItems(channel); - channel.register(ChannelTopics.CurrentChanged, symbol => this._currentChanged(symbol)); - channel.register(ChannelTopics.ItemAdded, item => this._itemAdded(item)); - channel.register(ChannelTopics.ItemRemoved, item => this._itemRemoved(item)); - channel.register(ChannelTopics.ItemChanged, item => { /* do nothing */ }); - channel.onDisconnection(channelInfo => this._handleDisconnect(channelInfo)); - return channel; - } - - _handleDisconnect(channelInfo) { - if (this._reconnecting) { - return; - } - this._reconnecting = true; - // handle the channel lifecycle here - we can connect again which will return a promise - // that will resolve if/when the channel is re-created. - console.log('Channels: disconnected'); - this._initChannel(); - } - - _mapToChartData(first, price) { + mapToChartData(first, price) { return { date: price.date, high: price.high, @@ -114,7 +81,7 @@ class Chart extends React.Component { }; } - _parsePrices(prices) { + parsePrices(prices) { return prices.map(p => { return { date: new Date(p.date), @@ -126,47 +93,11 @@ class Chart extends React.Component { }); } - _loadItems(channel) { - channel.dispatch(ChannelTopics.LoadItems).then(response => { - const current = response.current; - const items = response.items; - console.log('Channels: received response "loadItems". Items: ' + items.length); - items.forEach(item => { - let prices = this._parsePrices(item.prices); - this._portfolio.addItem(item.symbol, item.chart, item.name, item.color, prices); - }); - window.setTimeout(() => { - this._currentChanged(current); - }); - }); - - console.log('Channels: sent message "loadItems"'); - } - - _itemAdded(item) { - console.log('Channels: received message "itemAdded"'); - - let prices = this._parsePrices(item.prices); - this._portfolio.addItem(item.symbol, item.chart, item.name, item.color, prices); - } - - _itemRemoved(item) { - console.log('Channels: received message "itemRemoved"'); - - this._portfolio.removeItem(item.symbol); - } - - // update chart selection to match portfolio selection - _currentChanged(symbol) { - console.log('Channels: received message "currentChanged"'); - - /* eslint-disable-next-line eqeqeq */ - const current = this._portfolio.view.items.find(pi => pi.symbol == symbol); - - this.props.onColorChange(current.color); + handleCurrentChange(current) { this.setState({ current }); + this.props.onColorChange(current ? current.color : null); } } diff --git a/packages/stock-charts/src/index.css b/packages/stock-charts/src/index.css index 1c5d8d3..ff68897 100644 --- a/packages/stock-charts/src/index.css +++ b/packages/stock-charts/src/index.css @@ -18,6 +18,7 @@ body justify-content: space-between; padding: 10px; -webkit-app-region: drag; + transition: background-color linear 0.5s; } .panel-heading button { @@ -54,6 +55,38 @@ body margin: 0; padding-top: 0; padding-right: 0; - height: calc(100% - 5px); + height: 100%; overflow: hidden; +} + +.chart-container { + height: calc(100% - 5px); +} + +.chart-container.fadein { + animation: fadein ease-out 0.5s; +} + +.chart-container.fadeout { + animation: fadeout ease-in 0.5s; +} + +@keyframes fadein { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeout { + from { + opacity: 0; + } + + to { + opacity: 1; + } } \ No newline at end of file diff --git a/packages/stock-charts/src/trendline-chart/chart.js b/packages/stock-charts/src/trendline-chart/chart.js index 1e32723..99ce2bc 100644 --- a/packages/stock-charts/src/trendline-chart/chart.js +++ b/packages/stock-charts/src/trendline-chart/chart.js @@ -1,38 +1,35 @@ import React from 'react'; import * as wjChart from "@grapecity/wijmo.react.chart"; import * as wjChartAnalysis from "@grapecity/wijmo.react.chart.analytics"; -import { ChannelName, ChannelTopics, Portfolio } from 'stock-core'; +import { Portfolio } from 'stock-core'; +import { ChartController } from '../chartController'; import './chart.css'; -const fin = window.fin; - class Chart extends React.Component { - _portfolio; - _channelPromise; - - /** - * Indicates that disconnection occured and currently connection is in progress. - * It is used to avoid multiple connections in such cases. - */ - _reconnecting = false; constructor(props) { super(props); - this.state = { - current: null - }; - + this.chartRef = React.createRef(); + // create portfolio - this._portfolio = new Portfolio({ + this.portfolio = new Portfolio({ storageKey: this.props.storageKey, - mapToChartData: this._mapToChartData + mapToChartData: this.mapToChartData }); - this.props.onPeriodChange(this._portfolio.chartPeriod); - this.props.onColorChange(null); + this.controller = new ChartController({ + chartRef: this.chartRef, + portfolio: this.portfolio, + parsePrices: this.parsePrices, + handleCurrentChange: this.handleCurrentChange.bind(this) + }); - this._initChannel(); + this.state = { + current: null + }; + + this.props.onInitialize(this.portfolio.chartPeriod, null); } renderChartContent() { @@ -57,58 +54,29 @@ class Chart extends React.Component { } render() { - this._portfolio.chartPeriod = this.props.period; - return ( - - {this.renderChartContent()} - - - - + this.portfolio.chartPeriod = this.props.period; + return ( +
+ + {this.renderChartContent()} + + + + +
); } - _initChannel() { - if (typeof fin === 'undefined') { - console.warn('Channels cannot be initialized because "fin" is undefined.'); - return; - } - - this._channelPromise = fin.InterApplicationBus.Channel.connect(ChannelName) - .then(channel => this._handleConnect(channel)); - } - - _handleConnect(channel) { - this._reconnecting = false; - console.log('Channels: connected'); - this._loadItems(channel); - channel.register(ChannelTopics.CurrentChanged, symbol => this._currentChanged(symbol)); - channel.register(ChannelTopics.ItemAdded, item => this._itemAdded(item)); - channel.register(ChannelTopics.ItemRemoved, item => this._itemRemoved(item)); - channel.register(ChannelTopics.ItemChanged, item => { /* do nothing */ }); - channel.onDisconnection(channelInfo => this._handleDisconnect(channelInfo)); - return channel; - } - - _handleDisconnect(channelInfo) { - if (this._reconnecting) { - return; - } - this._reconnecting = true; - // handle the channel lifecycle here - we can connect again which will return a promise - // that will resolve if/when the channel is re-created. - console.log('Channels: disconnected'); - this._initChannel(); - } - - _mapToChartData(first, price) { + mapToChartData(first, price) { return { date: price.date, price: price.price }; } - _parsePrices(prices) { + parsePrices(prices) { return prices.map(p => { return { date: new Date(p.date), @@ -117,47 +85,11 @@ class Chart extends React.Component { }); } - _loadItems(channel) { - channel.dispatch(ChannelTopics.LoadItems).then(response => { - const current = response.current; - const items = response.items; - console.log('Channels: received response "loadItems". Items: ' + items.length); - items.forEach(item => { - let prices = this._parsePrices(item.prices); - this._portfolio.addItem(item.symbol, item.chart, item.name, item.color, prices); - }); - window.setTimeout(() => { - this._currentChanged(current); - }); - }); - - console.log('Channels: sent message "loadItems"'); - } - - _itemAdded(item) { - console.log('Channels: received message "itemAdded"'); - - let prices = this._parsePrices(item.prices); - this._portfolio.addItem(item.symbol, item.chart, item.name, item.color, prices); - } - - _itemRemoved(item) { - console.log('Channels: received message "itemRemoved"'); - - this._portfolio.removeItem(item.symbol); - } - - // update chart selection to match portfolio selection - _currentChanged(symbol) { - console.log('Channels: received message "currentChanged"'); - - /* eslint-disable-next-line eqeqeq */ - const current = this._portfolio.view.items.find(pi => pi.symbol == symbol); - - this.props.onColorChange(current.color); + handleCurrentChange(current) { this.setState({ current }); + this.props.onColorChange(current ? current.color : null); } } diff --git a/packages/stock-portfolio/projects/stock-portfolio-chart/src/app/stock-changes-chart/stock-changes-chart.component.html b/packages/stock-portfolio/projects/stock-portfolio-chart/src/app/stock-changes-chart/stock-changes-chart.component.html index 5283188..cfb1513 100644 --- a/packages/stock-portfolio/projects/stock-portfolio-chart/src/app/stock-changes-chart/stock-changes-chart.component.html +++ b/packages/stock-portfolio/projects/stock-portfolio-chart/src/app/stock-changes-chart/stock-changes-chart.component.html @@ -1,5 +1,5 @@
-
+
app logo  {{title}}
@@ -27,6 +27,7 @@ [chartType]="'Line'" [selectionMode]="'Series'" (selectionChanged)="selectionChanged(chart)" + (animationend)="handleAnimationEnd()" #chart> pi.symbol == symbol); - - const chart = this.chart; - if (chart) { - let selSeries = null; - for (let i = 0; i < chart.series.length; i++) { - if (chart.series[i].name == symbol) { - selSeries = chart.series[i]; - break; - } - } - chart.selection = selSeries; + + if (!this.chart) { + return; + } + + if (!this.currentSymbol) { + this.currentSymbol = symbol; + this._displaySymbol(this.currentSymbol); + return; + } + + if (symbol == this.currentSymbol) { + return; } + + this.currentSymbol = symbol; + + const chartElement = this.chart.hostElement; + chartElement.classList.remove('fadein', 'fadeout'); + chartElement.classList.add('fadein'); + } + + handleAnimationEnd() { + const chartElement = this.chart.hostElement; + if (chartElement.classList.contains('fadeout')) { + + chartElement.classList.remove('fadeout'); + } else if (chartElement.classList.contains('fadein')) { + + chartElement.classList.remove('fadein'); + chartElement.style.visibility = 'hidden'; + + this._displaySymbol(this.currentSymbol); + + chartElement.style.visibility = 'visible'; + chartElement.classList.add('fadeout'); + } + } + + private _getColor(symbol) { + const current = this.portfolio.view.items.find(pi => pi.symbol == symbol); + return current ? current.color : null; + } + + private _displaySymbol(symbol) { + const chartSeries = this.chart.series; + for (let i = 0; i < chartSeries.length; i++) { + if (chartSeries[i].name == symbol) { + this.chart.selection = chartSeries[i]; + break; + } + } + this.currentColor = this._getColor(symbol); } // send notifications about portfolio selection async selectionChanged(sender) { - const chart = sender; - const symbol = chart.selection ? chart.selection.name : null; + const selectedSeries = sender.selection; + this.currentSymbol = selectedSeries ? selectedSeries.name : null; + this.currentColor = this._getColor(this.currentSymbol); + const connection = await this._channelPromise; - connection.dispatch(ChannelTopics.SelectionChanged, symbol); + connection.dispatch(ChannelTopics.SelectionChanged, this.currentSymbol); console.log('Channels: sent message "selectionChanged"'); } diff --git a/packages/stock-portfolio/projects/stock-portfolio-chart/src/styles.css b/packages/stock-portfolio/projects/stock-portfolio-chart/src/styles.css index f5fd7e0..fe222cc 100644 --- a/packages/stock-portfolio/projects/stock-portfolio-chart/src/styles.css +++ b/packages/stock-portfolio/projects/stock-portfolio-chart/src/styles.css @@ -22,6 +22,7 @@ body justify-content: space-between; padding: 10px; -webkit-app-region: drag; + transition: background-color linear 0.5s; } .panel-heading button { @@ -62,6 +63,34 @@ body overflow: hidden; } +.wj-flexchart.fadein { + animation: fadein ease-out 0.5s; +} + +.wj-flexchart.fadeout { + animation: fadeout ease-in 0.5s; +} + .wj-flexchart .wj-state-selected { stroke-width: 6px; +} + +@keyframes fadein { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeout { + from { + opacity: 0; + } + + to { + opacity: 1; + } } \ No newline at end of file