tradingraph
Version:
Based on rpiontik chart https://github.com/rpiontik/crypto-chart
324 lines (313 loc) • 11 kB
JavaScript
const MIN_WIDTH_CANDLE = 10; // Минимальная ширина свечи
const VOLUME_ZONE = 0.3; // Область отводимая на объем
class CandlesWorker {
constructor () {
this.data = {
treeReady: false,
raw: [],
start: null,
width: null,
tree: [],
lastResolution: 0
};
this.averageData = [];
this.params = {
empty: false,
noMoreData: false,
dataRequestPending: false,
candleWidths: [],
firstTimestamp: 0,
lastTimestamp: 0,
defaultExposition: 86400 * 30
}
// console.log('constructor');
this.requestParams();
}
/**
* @description Send message to parrent
* @param {String} type - string based command for parrent
* @param {Object} body - data for message depends on command
*/
sendMessage (type, body = null) {
postMessage({type, body});
}
messageHandler (message) {
switch (message.data.task) {
case 'SET-PARAMS': {
this.setParams(message.data.params);
if (this.params.lastTimestamp > 0 && !this.params.dataRequestPending && !this.data.raw.length) {
this.initialLoading();
}
break;
}
case 'APPEND': {
this.append(message.data.data);
break;
}
case 'APPEND_AVERAGE': {
this.appendAverage(message.data.data);
break;
}
case 'RENDER': {
this.renderCandles(message.data.offset, message.data.exposition, message.data.viewWidth, message.data.viewHeight);
break;
}
case 'RENDER_AVERAGE': {
this.renderAverage(message.data.offset, message.data.exposition, message.data.viewWidth, message.data.viewHeight);
break;
}
case 'RELOAD': {
this.params.empty = false;
this.params.noMoreData = false;
this.resetData();
// this.params.dataRequestPending = true;
this.requestParams();
break;
}
default: break;
}
}
resetData () {
this.data.treeReady = false;
this.data.raw = [];
this.data.start = null;
this.data.width = null;
this.data.tree = [];
this.averageData = [];
this.params.dataRequestPending = false;
}
/**
* @description Apply params for render
* @param {Object} params - params
* @return none
*/
setParams (freshParams) {
Object.keys(freshParams).map((param) => {
this.params[param] = freshParams[param];
});
this.data.treeReady = false;
}
requestParams () {
this.sendMessage('NEED_PARAMS', {
inner: {
candleWidths: null
},
outer: {
firstTimestamp: null,
lastTimestamp: null
}
});
}
initialLoading () {
let exposition = this.params.defaultExposition;
let offset = this.params.lastTimestamp - exposition;
offset = offset > this.params.firstTimestamp ? offset : this.params.firstTimestamp;
this.params.dataRequestPending = true;
this.sendMessage('NEED_DATA', {offset, exposition});
}
/**
* @description Append part the chart data
* @param {Number} chartID - chart ID
* @param {Array} data - data of candles
* @return none
*/
append (data) {
// console.log('APPEND');
this.params.dataRequestPending = false;
if (data.length > 0) {
this.data.treeReady = false;
this.data.raw.splice(0);
this.data.raw = data.slice();
this.data.raw.sort((a, b) => { return a.date - b.date;});
} else if (!this.params.empty && !this.params.noMoreData) {
this.resetData();
}
this.sendMessage('APPENDED');
}
appendAverage (data) {
// console.log('APPEND AVERAGE');
this.averageData.splice(0);
this.averageData = data.slice();
this.sendMessage('APPENDED_AVERAGE');
}
/**
* @description Make specific tree by raw data
* @return none
*/
makeTree () {
if (this.data.raw.length > 0) {
this.data.start = this.data.raw[0].date;
this.data.end = this.data.raw[this.data.raw.length - 1].date;
this.params.candleWidths.map((case_) => {
this.data.tree[case_] = [];
let lastCandle = null;
this.data.raw.map((candle) => {
let id = candle.date - (candle.date % case_);
if (lastCandle && (id === lastCandle.id)) {
lastCandle.low = candle.low < lastCandle.low ? candle.low : lastCandle.low;
lastCandle.high = candle.high > lastCandle.high ? candle.high : lastCandle.high;
lastCandle.close = candle.close;
lastCandle.volume += candle.volume;
} else {
if (lastCandle) {
this.data.tree[case_].push(lastCandle);
}
lastCandle = {
id: id,
timestamp: candle.date,
open: candle.open,
low: candle.low,
high: candle.high,
close: candle.close,
volume: candle.volume
};
}
});
if (lastCandle) {
this.data.tree[case_].push(lastCandle);
}
});
// console.log(this.data.tree);
this.data.treeReady = true;
}
}
/**
* @description Looking for satisfying width of candle
* @param {Number} exposition - exposition width
* @param {Number} viewWidth - view box width
* @return true/false
*/
findCandleWidthForUse (exposition, viewWidth) {
let targetCandleNumber = viewWidth / MIN_WIDTH_CANDLE;
let caseCandidate = null;
let prevCandleDiff = 0;
this.params.candleWidths.map((case_) => {
if (caseCandidate) {
let candleDiff = Math.abs(Math.round(targetCandleNumber - exposition / case_));
if (candleDiff < prevCandleDiff) {
prevCandleDiff = candleDiff;
caseCandidate = case_;
}
} else {
caseCandidate = case_;
prevCandleDiff = Math.abs(Math.round(targetCandleNumber - exposition / case_));
}
});
return caseCandidate;
}
/**
* @description Render candles objects
* @param {Number} offset - exposition offset
* @param {Number} exposition - exposition width
* @param {Number} viewWidth - view box width
*/
renderCandles (offset, exposition, viewWidth, viewHeight) {
if (!this.data.treeReady) {
this.makeTree();
}
let result = {
low: null,
high: null,
maxVolume: null,
width: null,
candles: [],
candlesPositivePath: [],
candlesNegativePath: [],
volumePath: []
};
let theCase = this.findCandleWidthForUse(exposition, viewWidth);
let theData = this.data.tree[theCase];
let koofX = viewWidth / exposition;
result.width = theCase * koofX;
let start = 0;
// console.log('RENDER', offset, exposition, viewWidth, viewHeight);
if (theData && this.data.lastResolution === theCase) {
let stop = theData.length;
if (offset > this.data.start) {
start = -Math.floor((offset - this.data.start) / theCase);
}
for (let index = -start; index < stop; index++) {
let candle = theData[index];
if (candle.timestamp <= offset) {
continue;
} else if (candle.timestamp > offset + exposition) {
stop = index;
break;
} else if (start < 0) {
start = index;
}
if ((result.low == null) || (result.low > candle.low)) {
result.low = candle.low;
}
if ((result.high == null) || (result.high < candle.high)) {
result.high = candle.high;
}
if ((result.maxVolume == null) || (result.maxVolume < candle.volume)) {
result.maxVolume = candle.volume;
}
}
start = Math.abs(start);
if (stop == null) {
stop = theData.length;
}
let koofY = viewHeight / (result.high - result.low);
let koofYV = viewHeight * VOLUME_ZONE / result.maxVolume; // for volume
let barHalf = theCase * koofX * 0.25;
for (let index = start; index < stop; index++) {
let candle = theData[index];
let x = (candle.timestamp - offset) * koofX;
let pathMainLine = `M${x} ${(result.high - candle.low) * koofY} L${x} ${(result.high - candle.high) * koofY} `;
let pathCandleBody = `M${x - barHalf} ${(result.high - candle.close) * koofY} L${x + barHalf} ${(result.high - candle.close) * koofY} ` +
`L${x + barHalf} ${(result.high - candle.open) * koofY} L${x - barHalf} ${(result.high - candle.open) * koofY} `;
let rCandle = Object.assign({}, candle);
if (candle.open <= candle.close) {
rCandle.class = 'positive';
rCandle.candlePathIndex = result.candlesPositivePath.push(pathMainLine + pathCandleBody) - 1;
} else {
rCandle.class = 'negative';
rCandle.candlePathIndex = result.candlesNegativePath.push(pathMainLine + pathCandleBody) - 1;
}
rCandle.volumePathIndex = result.volumePath.push(`M${x - barHalf} ${viewHeight - candle.volume * koofYV} L${x + barHalf} ${viewHeight - candle.volume * koofYV} ` +
`L${x + barHalf} ${viewHeight} L${x - barHalf} ${viewHeight} `) - 1;
rCandle.x = x;
result.candles.push(rCandle);
}
} else if (!this.params.dataRequestPending) {
this.params.dataRequestPending = true;
this.sendMessage('NEED_DATA', {offset: offset, exposition: this.params.defaultExposition, resolution: theCase});
// this.data.lastResolution = theCase;
// return false;
}
if (this.data.start > 0 && this.data.start <= offset) {
} else if (!this.params.dataRequestPending && offset > this.params.firstTimestamp) {
this.params.dataRequestPending = true;
this.sendMessage('NEED_DATA', {offset: offset, exposition: this.params.defaultExposition, resolution: theCase});
}
this.data.lastResolution = theCase;
this.sendMessage('RENDERED', result);
}
renderAverage (offset, exposition, viewWidth, viewHeight) {
if (this.averageData.length) {
let dataLength = this.averageData.length;
let step = (viewWidth) / this.averageData.length;
let result = {
minTimestamp: this.averageData[0].date - 86400,
maxTimestamp: this.averageData[dataLength - 1].date,
path: []
};
let sortedByAverage = this.averageData.slice().sort((a, b) => {return a.average - b.average;});
let highest = sortedByAverage[dataLength - 1].average;
let lowest = sortedByAverage[0].average;
let yMultiplyer = viewHeight / (highest - lowest);
result.path.push(`M6 ${yMultiplyer * (highest - this.averageData[0].average)}`);
for (let i = 1; i < dataLength; i++) {
result.path.push(`L${step * i} ${yMultiplyer * (highest - this.averageData[i].average)}`);
}
this.sendMessage('RENDERED_AVERAGE', result);
}
}
}
let worker = new CandlesWorker();
onmessage = (data) => {
worker.messageHandler(data);
};