diff --git a/src/charts/chart.js b/src/charts/chart.js
index 7a43b08..09b8fd8 100644
--- a/src/charts/chart.js
+++ b/src/charts/chart.js
@@ -1,6 +1,7 @@
import {useChartOrderStore} from "@/orderbuild.js";
import {invokeCallbacks, prototype} from "@/common.js";
import {DataFeed, initFeeDropdown, lookupSymbol} from "@/charts/datafeed.js";
+import {intervalToSeconds} from "@/misc.js";
export let widget = null
export let chart = null
@@ -24,6 +25,11 @@ function changeSymbol(symbol) {
}
+function changeInterval(interval, _timeframe) {
+ useChartOrderStore().intervalSecs = intervalToSeconds(interval)
+}
+
+
/* TradingView event keystrings
const subscribeEvents = [
'toggle_sidebar', 'indicators_dialog', 'toggle_header', 'edit_object_dialog', 'chart_load_requested',
@@ -69,8 +75,10 @@ function initChart() {
console.log('init chart')
chart = widget.activeChart()
chart.crossHairMoved().subscribe(null, (point)=>setTimeout(()=>handleCrosshairMovement(point),0) )
- chart.onSymbolChanged().subscribe(null, changeSymbol);
+ chart.onSymbolChanged().subscribe(null, changeSymbol)
+ chart.onIntervalChanged().subscribe(null, changeInterval)
changeSymbol(chart.symbolExt())
+ changeInterval(widget.symbolInterval().interval)
useChartOrderStore().chartReady = true
console.log('chart ready')
}
@@ -242,7 +250,6 @@ function drawingEventWorker() {
const queue = drawingEventQueue
drawingEventQueue = []
propsEvents = {}
- // console.log('events locked', queue)
for (const [id, event] of queue) {
if (event === 'properties_changed')
propsEvents[id] = event
@@ -259,8 +266,18 @@ function drawingEventWorker() {
}
function doHandleDrawingEvent(id, event) {
- // console.log('drawing event', id, event)
- const shape = event === 'remove' ? null : chart.getShapeById(id);
+ console.log('drawing event', id, event)
+ let shape
+ if (event==='remove')
+ shape = null
+ else {
+ try {
+ shape = chart.getShapeById(id)
+ }
+ catch {
+ return
+ }
+ }
if (event === 'create') {
const co = useChartOrderStore();
const callbacks = co.drawingCallbacks
diff --git a/src/charts/shape.js b/src/charts/shape.js
index 8422b7e..c659989 100644
--- a/src/charts/shape.js
+++ b/src/charts/shape.js
@@ -3,6 +3,8 @@
import {invokeCallback, mixin} from "@/common.js";
import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape, widget} from "@/charts/chart.js";
import {unique} from "@/misc.js";
+import {useChartOrderStore} from "@/orderbuild.js";
+import model from "color";
//
@@ -37,34 +39,96 @@ export const ShapeType = {
}
-class Shape {
- constructor(type, model={}, onModel=null, onDelete=null, props=null) {
+export class Shape {
+
+ constructor(type, onModel=null, onDelete=null, props=null) {
// the Shape object manages synchronizing internal data with a corresponding TradingView shape
+
+ // each shape in the class hierarchy overrides setModel() to cause effects in TradingView
+ // TV callbacks are returned to the on*() handlers, primarily onPoints() and onProps(). Handlers
+ // of TV callbacks should call updateModel() with just the kv's they want to change. Any changes
+ // are then passed onto the onModel() callback.
+
+ // the model object has various attributes defined by each shape subclass. each subclass's constructor
+ // must call setModel(model) after first calling super.constructor() and optionally declaring any model
+ // defaults on this.model.
+
+ // Shape: {
+ // lineColor, textColor, color, // lineColor and textColor default to color
+ // }
+
+ this.debug = false
this.id = null // TradingView shapeId, or null if no TV shape created yet (drawing mode)
this.type = type // ShapeType
- this.model = model // subclass-specific
- this.points = null
- this.points = this.pointsFromModel()
- // console.log('construct points', this.points)
- this.props = props === null ? this.propsFromModel() : mixin(props, this.propsFromModel())
+ this.model = {} // set to nothing at first
+ this.ourPoints = null
+ this.tvPoints = null
+ this.ourProps = {}
+ if (props !== null)
+ this.ourProps = mixin(props, this.ourProps)
+ this.tvProps = null
if (onModel !== null)
this.onModel = onModel
if (onDelete !== null )
this.onDelete = onDelete
- this.create()
+
+ // Model values handled by this base class
+ this.model.color = null
+ this.model.lineColor = null
+ this.model.textColor = null
+
}
//
// primary interface methods
//
+ setModel(model) {
+ if (model.textColor || model.lineColor) {
+ if (model.textColor)
+ this.model.textColor = model.textColor
+ if (model.lineColor)
+ this.model.lineColor = model.lineColor
+ this.setProps(this.colorProps())
+ }
+ // todo text
+ }
+
+ onModel(model, changedKeys) {} // model was changed by a TradingView user action
+
+ updateModel(changes) {
+ if (this.debug) console.log('updateModel', this.id, changes)
+ const changedKeys = []
+ for (const k of Object.keys(changes)) {
+ if( changes[k] !== undefined && changes[k] !== this.model[k] ) {
+ changedKeys.push(k)
+ this.model[k] = changes[k]
+ }
+ }
+ if (changedKeys.length)
+ this.onModel(this.model, changedKeys)
+ }
+
+ colorProps() {
+ if (!this.model.color&&!this.model.lineColor&&!this.model.textColor) return null
+ const o = {}
+ const p = this.type.drawingProp
+ const lc = this.model.lineColor ? this.model.lineColor : this.model.color;
+ const tc = this.model.textColor ? this.model.textColor : this.model.color;
+ if (lc)
+ o[p+".linecolor"] = lc
+ if (tc)
+ o[p+".textcolor"] = tc
+ return o
+ }
+
draw() {
// have the user draw this shape
- console.log(`draw ${this.type.name}`, this.model)
+ if (this.debug) console.log(`draw ${this.type.name}`, this.model)
if (this.id)
throw Error(`Shape already exists ${this.id}`)
const or = this.drawingOverrides();
- // console.log('drawing overrides', or)
+ // if (this.debug) console.log('drawing overrides', or)
widget.applyOverrides(or)
drawShape(this.type, new ShapeTVCallbacks(this))
}
@@ -72,30 +136,28 @@ class Shape {
// return an object with property defaults, e.g. { "linetoolhorzline.linecolor": "#7f11e0" }
// https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library#drawingoverrides
drawingOverrides() {
- if (this.model.color===null) return null
- const o = {}
- const p = this.type.drawingProp
- o[p+".linecolor"] = this.model.color
- o[p+".textcolor"] = this.model.color
- return o
+ return this.colorProps()
}
create() {
if (this.id !== null) return
// programatically create the shape using the current this.points
- if( this.points && this.points.length ) {
+ if( this.ourPoints && this.ourPoints.length ) {
this.doCreate()
}
}
doCreate() {
- createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
+ // createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
+ this.tvPoints = [...this.ourPoints]
+ this.id = createShape(this.type, this.ourPoints, {overrides:this.ourProps}, new ShapeTVCallbacks(this))
+ if (this.debug) console.log('created', this.type.name, this.ourPoints, this.id)
}
createOrDraw() {
if(this.id) return
- if(this.points && this.points.length)
- this.doCreate(this.points)
+ if(this.ourPoints && this.ourPoints.length)
+ this.doCreate(this.ourPoints)
else
this.draw()
}
@@ -106,36 +168,25 @@ class Shape {
}
- setModel(model, props=null) {
- for( const [k,v] of Object.entries(model))
- this.model[k] = v
- this.setPointsIfDirty(this.pointsFromModel());
- let p
- const mp = this.propsFromModel();
- if (props===null) {
- p = mp
- if (p===null)
- return
- }
- else if (mp !== null)
- p = mixin(props, mp)
- this.setPropsIfDirty(p)
- }
-
-
setPoints(points) {
- this.points = points
+ // setting points to null will delete the shape from the chart. setting points to a valid value will cause the
+ // shape to be drawn.
+ if (this.debug) console.log('setPoints', points)
+ this.ourPoints = points
if (points === null || !points.length)
this.delete()
else {
if (this.id === null)
this.doCreate()
- else {
+ else if (dirtyPoints(this.tvPoints, points)) {
const s = this.tvShape();
- if (!dragging) {
+ if (this.debug) console.log('adjusting tv points', s, this.tvPoints, points)
+ if (!this.beingDragged()) {
+ if (this.debug) console.log('not dragging. use setPoints.')
s.setPoints(points)
}
else {
+ if (this.debug) console.log('dragging. use QUIET setPoints.')
// quiet setPoints doesn't disturb tool editing mode
const i = s._pointsConverter.apiPointsToDataSource(points)
// s._model.startChangingLinetool(this._source)
@@ -147,67 +198,39 @@ class Shape {
}
}
-
- setPointsIfDirty(points) {
- if (dirtyPoints(this.points, points))
- this.setPoints(points)
- }
-
+ onPoints(points) {} // the control points of an existing shape were changed
setProps(props) {
- if(this.id)
- this.tvShape().setProperties(props)
+ if (!props || Object.keys(props).length===0) return
+ if (this.debug) console.log('setProps', props)
+ this.ourProps = mixin(props, this.ourProps)
+ if(this.id) {
+ const p = dirtyItems(this.tvProps, props)
+ this.tvProps = this.tvProps === null ? p : mixin(p, this.tvProps)
+ this.tvShape().setProperties(p)
+ }
}
-
- setPropsIfDirty(props) {
- // console.log('dirtyProps', this.props, props, dirtyProps(this.props, props))
- if( dirtyProps(this.props, props) )
- this.setProps(props)
+ onProps(props) { // the display properties of an existing shape were changed
+ this.updateModel({lineColor:props.linecolor, textColor:props.textcolor})
}
-
beingDragged() {
return draggingShapeIds.indexOf(this.id) !== -1
}
delete() {
- // console.log('shape.delete', this.id)
+ if (this.debug) console.log('shape.delete', this.id)
+ this.ourPoints = null
if (this.id === null) return
- this.lock++
- try {
- deleteShapeId(this.id)
- this.id = null
- } finally {
- this.lock--
- }
+ deleteShapeId(this.id)
+ this.id = null
}
//
- // Model synchronization
- //
-
- onDirtyModel(toModel) {
- const old = {...this.model}
- toModel.call(this)
- const dirty = dirtyKeys(old, this.model)
- // console.log('onDirtyModel', old, this.model, dirty)
- if (dirty.length)
- this.onModel(this.model)
- }
-
- pointsFromModel() {return null}
- pointsToModel() {} // set the model using this.points
- propsFromModel() {return null}
- propsToModel() {} // set the model using this.props
-
- onModel(model) {} // called whenever points or props updates the model dictionary
-
-
- //
- // Overridable shape callbacks
+ // Overridable shape callbacks initiated by TradingView
//
onDraw() {} // start drawing a new shape for the builder
@@ -215,8 +238,6 @@ class Shape {
onUndraw() {} // drawing was canceled by clicking on a different tool
onAddPoint() {} // the user clicked a point while drawing (that point is added to the points list)
onCreate(points, props) {} // the user has finished creating all the control points. drawing mode is exited and the initial shape is created.
- onPoints(points) {} // the control points of an existing shape were changed
- onProps(props) {} // the display properties of an existing shape were changed
onMove(points) {} // the shape was moved by dragging a drawing element not the control point
onDrag(points) {}
onHide(props) {}
@@ -226,7 +247,6 @@ class Shape {
}
-
function dirtyPoints(pointsA, pointsB) {
if (pointsB===null)
return pointsA !== null
@@ -243,21 +263,6 @@ function dirtyPoints(pointsA, pointsB) {
return false
}
-
-// B is modifying A
-function dirtyProps(propsA, propsB) {
- if (propsB===null)
- return propsA !== null
- const entries = Object.entries(propsB);
- if (propsA===null)
- return entries.length > 0
- for( const [k,v] of entries)
- if ( !(k in propsA) || propsA[k] !== v )
- return true
- return false
-}
-
-
// B is modifying A
function dirtyKeys(propsA, propsB) {
if (propsB===null)
@@ -265,7 +270,15 @@ function dirtyKeys(propsA, propsB) {
if (propsA===null)
return [...Object.keys(propsB)]
const keys = unique([...Object.keys(propsA), ...Object.keys(propsB)])
- return keys.filter((k)=> !(k in propsA) || propsA[k] !== propsB[k])
+ return keys.filter((k)=> !(k in propsA) || propsA[k] !== undefined && propsA[k] !== propsB[k])
+}
+
+
+function dirtyItems(a, b) {
+ const result = {}
+ for (const k of dirtyKeys(this.tvProps, props))
+ result[k] = props[k]
+ return result
}
@@ -278,113 +291,173 @@ class ShapeTVCallbacks {
}
onCreate(shapeId, _tvShape, points, props) {
- if( this.shape.id )
- throw Error(`Created a shape ${shapeId} where one already existed ${this.shape.id}`)
- this.shape.id = shapeId
- if( this.shape.lock ) return
this.creating = true
invokeCallback(this.shape, 'onCreate', points, props)
}
onPoints(shapeId, _tvShape, points) {
- this.shape.points = points
+ if (this.shape.debug) console.log('tvcb onPoints', points)
+ this.shape.tvPoints = points
this.shape.onPoints(points)
- this.shape.onDirtyModel(this.shape.pointsToModel)
}
onProps(shapeId, _tvShape, props) {
- // console.log('onProps', props)
- if (this.creating) {
+ // if (this.shape.debug) console.log('tvOnProps', props)
+ if (this.creating) { // todo still useful?
this.creating = false
return
}
- this.shape.props = props
+ this.shape.tvProps = props
this.shape.onProps(props)
- this.shape.onDirtyModel(this.shape.propsToModel)
}
onDraw() {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onDraw')
}
onRedraw() {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onRedraw')
}
onUndraw() {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onUndraw')
}
onAddPoint() {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onAddPoint')
}
onMove(_shapeId, _tvShape, points) {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onMove',points)
}
onDrag(_shapeId, _tvShape, points) {
- if( this.shape.lock ) return
+ if (this.shape.debug) console.log('onDrag')
invokeCallback(this.shape, 'onDrag', points)
}
onHide(_shapeId, _tvShape, props) {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onHide',props)
}
onShow(_shapeId, _tvShape, props) {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onShow',props)
}
onClick(_shapeId, _tvShape) {
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onClick')
}
onDelete(shapeId) {
this.shape.id = null
- if( this.shape.lock ) return
invokeCallback(this.shape, 'onDelete', shapeId)
}
}
-export class HLine extends Shape {
+export class Line extends Shape {
+ onDrag(points) {
+ const s = this.tvShape();
+ if (this.debug) {
+ console.log('shape', s)
+ console.log('currentMovingPoint', s._source.currentMovingPoint())
+ console.log('startMovingPoint', s._source.startMovingPoint())
+ console.log('isBeingEdited', s._source.isBeingEdited())
+ console.log('state', s._source.state())
+ }
+ }
+}
+
+
+export class HLine extends Line {
constructor(model, onModel=null, onDelete=null, props=null) {
super(ShapeType.HLine, model, onModel, onDelete, props)
}
- pointsFromModel() {
- if (this.model.price === null) return null
- const time = this.points !== null && this.points.length > 0 ? this.points[0].time : 0
- return [{time:time, price:this.model.price}]
+ setModel(model) {
+ if (model.price !== undefined && model.price !== this.model.price) {
+ this.model.price = model.price
+ this.setPoints([{time:0,price:this.model.price}])
+ }
}
- pointsToModel() {
- this.model.price = this.points[0].price
+ onPoints(points) {
+ super.onPoints(points);
}
- propsFromModel() {
- return this.model.color ? {linecolor: this.model.color} : null
+ pointsFromModel(model) {
+ if (model.price === null || model.price===undefined) return null
+ // take any time available, or 0
+ const time =
+ this.ourPoints !== null && this.ourPoints.length > 0 ? this.ourPoints[0].time :
+ this.tvPoints !== null && this.tvPoints.length > 0 ? this.tvPoints[0].time : 0
+ return [{time:time, price:model.price}]
}
- propsToModel() {this.model.color=this.props.linecolor}
-
- onDrag(points) {
- const s = this.tvShape();
- console.log('shape', s)
- console.log('currentMovingPoint', s._source.currentMovingPoint())
- console.log('startMovingPoint', s._source.startMovingPoint())
- console.log('isBeingEdited', s._source.isBeingEdited())
- console.log('state', s._source.state())
+ pointsToModel(points) {
+ return {price: this.ourPoints[0].price}
}
}
+
+function timeAdjustmentTooSmall(orig, newValue) {
+ // TradingView adjusts our lines to be at the start of the intervals, so
+ // we ignore deltas smaller than one interval prior
+ return newValue === undefined ||
+ orig !== null && orig !== undefined && newValue !== null &&
+ newValue < orig && orig - newValue < useChartOrderStore().intervalSecs
+}
+
+
+function ohlcStart(time) {
+ const period = useChartOrderStore().intervalSecs
+ return Math.floor(time/period) * period
+}
+
+
+export class VLine extends Line {
+ constructor(model, onModel=null, onDelete=null, props=null) {
+ super(ShapeType.VLine, onModel, onDelete, props)
+
+ // Model
+ this.model.time = null
+
+ this.setModel(model) // call setModel at the end
+ }
+
+ onPoints(points) {
+ if (this.debug) console.log('vline onPoints', this.ourPoints, points)
+ super.onPoints(points);
+ const orig = this.ourPoints && this.ourPoints.length ? this.ourPoints[0].time : null
+ if (!timeAdjustmentTooSmall(orig, points[0].time)) {
+ if (this.debug) console.log('updateModel', points[0].time)
+ this.updateModel({time: points[0].time})
+ }
+ }
+
+ setModel(model) {
+ if (this.debug) console.log('vline setModel', this.model.time, model )
+ super.setModel(model)
+ if (model.time !== undefined && model.time !== this.model.time) {
+ this.model.time = model.time
+ const time = ohlcStart(model.time);
+ if (this.debug) console.log('vline setPoints', this.id, time)
+ this.setPoints([{time, price:1}])
+ }
+ }
+
+ delete() {
+ this.model.time = null
+ super.delete()
+ }
+
+ dirtyPoints(pointsA, pointsB) {
+ const a = pointsA ? pointsA[0].time : null
+ const b = pointsB ? pointsB[0].time : null
+ const result = !timeAdjustmentTooSmall(a, b)
+ if (this.debug) console.log('vline dirty points?', a, b, result)
+ return result
+ }
+}
+
diff --git a/src/common.js b/src/common.js
index 0f5a73d..be40122 100644
--- a/src/common.js
+++ b/src/common.js
@@ -3,7 +3,7 @@ export function mixin(child, ...parents) {
// assigned by parents order, highest priority first
for( const parent of parents ) {
for ( const key in parent) {
- if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key)) {
+ if (parent.hasOwnProperty(key) && !child.hasOwnProperty(key) && parent[key] !== undefined) {
child[key] = parent[key];
}
}
@@ -11,7 +11,6 @@ export function mixin(child, ...parents) {
return child;
}
-
export function prototype(parent, child) {
const result = Object.create(parent);
Object.assign(result, child)
diff --git a/src/components/chart/BuilderPanel.vue b/src/components/chart/BuilderPanel.vue
index 2be65bf..40dc444 100644
--- a/src/components/chart/BuilderPanel.vue
+++ b/src/components/chart/BuilderPanel.vue
@@ -1,28 +1,65 @@
-