Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V14 #27

Merged
merged 10 commits into from
Feb 5, 2021
2 changes: 1 addition & 1 deletion [email protected]/components/cards/stockCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ var StockCard = GObject.registerClass({
}

_createPostMarketAdditionalInformationBox () {
const quoteColorStyleClass = getStockColorStyleClass(this.cardItem.PreMarketChange)
const quoteColorStyleClass = getStockColorStyleClass(this.cardItem.PostMarketChange)

const additionalInformationBox = new St.BoxLayout({
style_class: 'info-section-box tar',
Expand Down
112 changes: 97 additions & 15 deletions [email protected]/components/chart/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { Clutter, GObject, St } = imports.gi
const ExtensionUtils = imports.misc.extensionUtils
const Me = ExtensionUtils.getCurrentExtension()

const { closest, isNullOrEmpty, isNullOrUndefined } = Me.imports.helpers.data
const { closest, fallbackIfNaN, isNullOrEmpty, isNullOrUndefined, getComplementaryColor } = Me.imports.helpers.data

var Chart = GObject.registerClass({
GTypeName: 'StockExtension_Chart',
Expand All @@ -13,16 +13,21 @@ var Chart = GObject.registerClass({
}
}
}, class Chart extends St.DrawingArea {
_init ({ data, x1, x2 }) {
_init ({ data, x1, x2, barData }) {
super._init({
style_class: 'chart',
reactive: true
})

this.data = data
this.barData = barData

this.x1 = x1
this.x2 = x2

this._selectedX = null
this._selectedY = null

this.connect('repaint', this._draw.bind(this))
this.connect('motion-event', (item, event) => this._onHover(item, event))
}
Expand All @@ -39,28 +44,46 @@ var Chart = GObject.registerClass({
this.width = width
this.height = height

// get primary color from themes
const themeNode = this.get_theme_node()

// FIXME: it would be nice to have some basic color sets in gnome-shell
const fgColor = themeNode.get_foreground_color()
const newColorString = getComplementaryColor(fgColor.to_string().slice(1, 7), false)
const secondaryColor = Clutter.color_from_string(`${newColorString}ff`)[1]

const baseParams = {
cairoContext,
width,
height,
primaryColor: fgColor,
secondaryColor: secondaryColor
}

this._draw_line_chart(baseParams)
this._draw_volume_bars(baseParams)
this._draw_crosshair(baseParams)

// dispose cairo stuff
cairoContext.$dispose()
}

_draw_line_chart ({ width, height, cairoContext, primaryColor }) {
// scale data to width / height of our cairo canvas
const seriesData = this._transformSeriesData(this.data, width, height)

// get primary color from themes
const fgColor = themeNode.get_foreground_color()
Clutter.cairo_set_source_color(cairoContext, fgColor)
Clutter.cairo_set_source_color(cairoContext, primaryColor)

// get first data
const [firstValueX, firstValueY] = [0, 0]

// tell cairo where to start drawing
cairoContext.moveTo(
firstValueX,
height - firstValueY
)
cairoContext.moveTo(firstValueX, height - firstValueY)

let lastValueX = firstValueX

seriesData.forEach(([valueX, valueY]) => {
if (isNullOrUndefined(valueX) || isNullOrUndefined(valueY)) {
if (isNullOrUndefined(valueX) || isNullOrUndefined(fallbackIfNaN(valueY, null))) {
return
}

Expand All @@ -76,9 +99,59 @@ var Chart = GObject.registerClass({

// render
cairoContext.fill()
}

// dispose cairo stuff
cairoContext.$dispose()
_draw_volume_bars ({ width, height, cairoContext, secondaryColor }) {
if (isNullOrEmpty(this.barData)) {
return
}

const volumeBarsHeight = height * 0.20 // use the 20% space at bottom
const seriesData = this._transformSeriesData(this.barData, width, volumeBarsHeight)

const barWidth = 3
const barWidthPerSide = barWidth / 3 // left, middle, right

Clutter.cairo_set_source_color(cairoContext, secondaryColor)

cairoContext.moveTo(0, height)

seriesData.forEach(([valueX, valueY]) => {
if (isNullOrUndefined(valueX) || isNullOrUndefined(fallbackIfNaN(valueY, null))) {
return
}

const x_start = valueX - barWidthPerSide
const x_end = valueX + barWidthPerSide

cairoContext.lineTo(x_start, height)
cairoContext.lineTo(x_start, height - valueY)
cairoContext.lineTo(x_end, height - valueY)
cairoContext.lineTo(x_end, height)
})

cairoContext.lineTo(0, height)
cairoContext.fill()
}

_draw_crosshair ({ width, height, cairoContext, secondaryColor }) {
if (this._selectedX) {
Clutter.cairo_set_source_color(cairoContext, secondaryColor)
cairoContext.moveTo(this._selectedX - 1, 0)
cairoContext.lineTo(this._selectedX - 1, height)
cairoContext.lineTo(this._selectedX, height)
cairoContext.lineTo(this._selectedX, 0)
cairoContext.fill()
}

if (this._selectedY) {
Clutter.cairo_set_source_color(cairoContext, secondaryColor)
cairoContext.moveTo(0, this._selectedY - 1)
cairoContext.lineTo(width, this._selectedY - 1)
cairoContext.lineTo(width, this._selectedY)
cairoContext.lineTo(0, this._selectedY)
cairoContext.fill()
}
}

_transformSeriesData (data, width, height) {
Expand All @@ -91,9 +164,12 @@ var Chart = GObject.registerClass({

const yValues = [...data.filter(item => item[1] !== null).map(item => item[1])]

const minValueY = Math.min(...yValues)
let minValueY = Math.min(...yValues)
const maxValueY = Math.max(...yValues)

// add small buffer to bottom
minValueY -= (maxValueY - minValueY) * 0.25

return data.map(([x, y]) => [
this.encodeValue(x, minValueX, maxValueX, 0, width),
isNullOrUndefined(y) ? null : this.encodeValue(y, minValueY, maxValueY, 0, height)
Expand All @@ -109,10 +185,11 @@ var Chart = GObject.registerClass({
// then convert the position data back to original x value (timestamp)
// find by this timestamp the closest item in series data

const [coordX] = event.get_coords()
const [positionX] = item.get_transformed_position()
const [coordX, coordY] = event.get_coords()
const [positionX, positionY] = item.get_transformed_position()

const chartX = coordX - positionX
const chartY = coordY - positionY
const minX = this.x1 || this.data[0][0]
const maxX = this.x2 || this.data[this.data.length - 1][0]

Expand All @@ -121,6 +198,11 @@ var Chart = GObject.registerClass({

const tsItem = this.data.find(data => data[0] === originalValueX)
this.emit('chart-hover', tsItem[0], tsItem[1])

this._selectedX = chartX
this._selectedY = chartY

this.queue_repaint()
}

// thx: https://stackoverflow.com/a/5732390/3828502
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ var StockDetailsScreen = GObject.registerClass({}, class StockDetailsScreen exte
this._sync()
})

const chart = new Chart({ data: quoteHistorical.Data, x1: quoteHistorical.MarketStart, x2: quoteHistorical.MarketEnd })
const chart = new Chart({
data: quoteHistorical.Data,
x1: quoteHistorical.MarketStart,
x2: quoteHistorical.MarketEnd,
barData: quoteHistorical.VolumeData
})

const chartValueLabel = new St.Label({ style_class: 'chart-hover-label', text: `` })

Expand Down
37 changes: 35 additions & 2 deletions [email protected]/helpers/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const CACHE_TIME = 10 * 1000

var isNullOrUndefined = value => typeof value === 'undefined' || value === null
var isNullOrEmpty = value => isNullOrUndefined(value) || value.length === 0
var fallbackIfNaN = value => typeof value === 'undefined' || value === null || isNaN(value) ? '--' : value
var fallbackIfNaN = (value, fallback = '--') => typeof value === 'undefined' || value === null || isNaN(value) ? fallback : value

var closest = (array, target) => array.reduce((prev, curr) => Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev)

Expand Down Expand Up @@ -56,5 +56,38 @@ var getStockColorStyleClass = change => {
return quoteColorStyleClass
}

var roundOrDefault = (number, defaultValue = '--') => isNullOrUndefined(number) ? defaultValue : (Math.round((number + Number.EPSILON) * 100) / 100).toFixed(2)
var getComplementaryColor = (hex, bw = true) => {
const padZero = (str, len) => {
len = len || 2
var zeros = new Array(len).join('0')
return (zeros + str).slice(-len)
}

if (hex.indexOf('#') === 0) {
hex = hex.slice(1)
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.')
}
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16)
if (bw) {
// http://stackoverflow.com/a/3943023/112731
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF'
}
// invert color components
r = (255 - r).toString(16)
g = (255 - g).toString(16)
b = (255 - b).toString(16)
// pad each with zeros and return
return '#' + padZero(r) + padZero(g) + padZero(b)
}

var roundOrDefault = (number, defaultValue = '--') => isNullOrUndefined(number) ? defaultValue : (Math.round((number + Number.EPSILON) * 100) / 100).toFixed(2)
2 changes: 1 addition & 1 deletion [email protected]/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
],
"url": "https://github.com/cinatic/stocks-extension",
"uuid": "[email protected]",
"version": 13
"version": 14
}
10 changes: 9 additions & 1 deletion [email protected]/services/dto/quoteHistorical.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var QuoteHistorical = class QuoteSummary {
this.MarketStart = null
this.MarketEnd = null
this.Data = []
this.VolumeData = []
this.Error = null
}
}
Expand All @@ -16,8 +17,15 @@ var createQuoteHistoricalFromYahooData = (responseData, error) => {
const result = responseData.chart.result[0]
const timestamps = result.timestamp || []
const quotes = (result.indicators.quote || [])[0].close || []
const volumes = (result.indicators.quote || [])[0].volume || []

newObject.Data = timestamps.map((timestamp, index) => [timestamp * 1000, quotes[index]])
newObject.Data = []
newObject.VolumeData = []

timestamps.forEach((timestamp, index) => {
newObject.Data.push([timestamp * 1000, quotes[index]])
newObject.VolumeData.push([timestamp * 1000, volumes[index]])
})

if (result.meta && result.meta.tradingPeriods) {
// there can be multiple tradingPeriods and multiple entries inside a tradingPeriod for each time series entry
Expand Down
14 changes: 14 additions & 0 deletions [email protected]/services/dto/quoteSummary.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
const ExtensionUtils = imports.misc.extensionUtils
const Me = ExtensionUtils.getCurrentExtension()

const { isNullOrUndefined } = Me.imports.helpers.data
const { MARKET_STATES } = Me.imports.services.meta.yahoo

var QuoteSummary = class QuoteSummary {
constructor (symbol) {
this.Name = null
Expand Down Expand Up @@ -76,6 +82,14 @@ var createQuoteSummaryFromYahooData = (symbol, quoteData, error) => {
if (priceData.postMarketTime) {
newObject.PostMarketTimestamp = priceData.postMarketTime * 1000
}

if(newObject.MarketState === MARKET_STATES.PRE && isNullOrUndefined(newObject.PreMarketPrice)){
newObject.MarketState = MARKET_STATES.PRE_WITHOUT_DATA
}

if(newObject.MarketState === MARKET_STATES.POST && isNullOrUndefined(newObject.PostMarketPrice)){
newObject.MarketState = MARKET_STATES.POST_WITHOUT_DATA
}
}

if (quoteData.quoteSummary.error && quoteData.quoteSummary.error.description) {
Expand Down
21 changes: 12 additions & 9 deletions [email protected]/services/meta/yahoo.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ var CHART_RANGES = {
MAX: 'max'
}

// "optimal" roll up for volume bars ~200 items
var INTERVAL_MAPPINGS = {
[CHART_RANGES.INTRADAY]: '1m',
[CHART_RANGES.WEEK]: '15m',
[CHART_RANGES.MONTH]: '1h',
[CHART_RANGES.HALF_YEAR]: '1h',
[CHART_RANGES.YEAR_TO_DATE]: '1h',
[CHART_RANGES.YEAR]: '1d',
[CHART_RANGES.FIVE_YEARS]: '1d',
[CHART_RANGES.MAX]: '1d',
[CHART_RANGES.INTRADAY]: '1m', // 4m roll up volume data
[CHART_RANGES.WEEK]: '5m', // 5m roll up volume data
[CHART_RANGES.MONTH]: '5m', // 4h roll up volume data
[CHART_RANGES.HALF_YEAR]: '1h', // 24h roll up volume data
[CHART_RANGES.YEAR_TO_DATE]: '1h', // 24h roll up volume data
[CHART_RANGES.YEAR]: '1d', // 48h roll up volume data
[CHART_RANGES.FIVE_YEARS]: '1d', // 240h roll up volume data
[CHART_RANGES.MAX]: '1d', // 480h roll up volume data
}

var MARKET_STATES = {
POST: 'POST',
PRE: 'PRE',
PRE_WITHOUT_DATA: 'POST_WITHOUT_DATA',
POST: 'POST',
POST_WITHOUT_DATA: 'POST_WITHOUT_DATA',
REGULAR: 'REGULAR',
}