Platforms to show: All Mac Windows Linux Cross-Platform
/ChartDirector/RealTime ViewPort
Required plugins for this example: MBS ChartDirector Plugin
You find this example project in your Plugins Download as a Xojo project file within the examples folder: /ChartDirector/RealTime ViewPort
This example is the version from Fri, 9th Feb 2023.
Project "RealTime ViewPort.xojo_binary_project"
Class App Inherits Application
Const kEditClear = "&Delete"
Const kFileQuit = "&Quit"
Const kFileQuitShortcut = ""
EventHandler Sub Open()
// CDBaseChartMBS.set License Code ...
End EventHandler
End Class
Class RealTimeWindow Inherits Window
Const initialFullRange = 180
Const initialVisibleRange = 30
Const sampleSize = 10000
Const zoomInLimit = 5
Control savePB Inherits BevelButton
ControlInstance savePB Inherits BevelButton
EventHandler Sub Action()
onSave
End EventHandler
End Control
Control zoomOutPB Inherits BevelButton
ControlInstance zoomOutPB Inherits BevelButton
EventHandler Sub Action()
If Me.Value Then
onMouseUsageChanged ChartViewer.MouseUsageZoomOut
pointerPB.Value = False
zoomInPB.Value = False
End If
End EventHandler
End Control
Control zoomInPB Inherits BevelButton
ControlInstance zoomInPB Inherits BevelButton
EventHandler Sub Action()
If Me.Value Then
onMouseUsageChanged ChartViewer.MouseUsageZoomIn
pointerPB.Value = False
zoomOutPB.Value = False
End If
End EventHandler
End Control
Control pointerPB Inherits BevelButton
ControlInstance pointerPB Inherits BevelButton
EventHandler Sub Action()
If Me.Value Then
onMouseUsageChanged ChartViewer.MouseUsageDefault
zoomInPB.Value = False
zoomOutPB.Value = False
End If
End EventHandler
End Control
Control ChartViewer Inherits ChartViewerCanvas
ControlInstance ChartViewer Inherits ChartViewerCanvas
EventHandler Sub mouseMovePlotArea(MouseEvent as MouseEvent)
self.onMouseMovePlotArea MouseEvent
End EventHandler
EventHandler Sub viewPortChanged()
Self.onViewPortChanged
ViewPortControl.onViewPortChanged
End EventHandler
End Control
Control ChartUpdateTimer Inherits Timer
ControlInstance ChartUpdateTimer Inherits Timer
EventHandler Sub Action()
onChartUpdateTimer
End EventHandler
End Control
Control LeftLine Inherits Line
ControlInstance LeftLine Inherits Line
End Control
Control TopLine Inherits Line
ControlInstance TopLine Inherits Line
End Control
Control BottomLine Inherits Line
ControlInstance BottomLine Inherits Line
End Control
Control RightLine Inherits Line
ControlInstance RightLine Inherits Line
End Control
Control SecondCanvas Inherits ViewPortControlCanvas
ControlInstance SecondCanvas Inherits ViewPortControlCanvas
End Control
Control HelpLabel Inherits Label
ControlInstance HelpLabel Inherits Label
End Control
EventHandler Sub Close()
dataSource.stopThread
If ViewPortControl <> Nil Then
ViewPortControl.Close
ViewPortControl = Nil
End If
End EventHandler
EventHandler Sub Open()
Redim Self.timeStamps(sampleSize-1)
Redim self.dataSeriesA(sampleSize-1)
Redim self.dataSeriesB(sampleSize-1)
Setup
End EventHandler
Sub InitRandomWalk()
dataSource = New RandomWalk
AddHandler dataSource.GotNewValues, WeakAddressOf onData
dataSource.run
End Sub
Sub Setup()
// connect line controls
ChartViewer.LeftLine = LeftLine
ChartViewer.RightLine = RightLine
ChartViewer.TopLine = TopLine
ChartViewer.BottomLine = BottomLine
// Pointer push button
pointerPB.Value = True
// Zoom In push button
zoomInPB.Value = False
// Zoom Out push button
zoomOutPB.Value = False
// Save push button
// Chart Viewer
'ChartViewer.setGeometry(0, 0, 640, 350)
// Viewport Control
ViewPortControl = New ViewPortControl
ViewPortControl.Output = SecondCanvas // <- for output in Xojo
ViewPortControl.setViewer(ChartViewer)
SecondCanvas.ViewPortControl = ViewPortControl
ChartViewer.HelpLabel = HelpLabel
//
// Initialize member variables
//
Self.currentIndex = 0
// Initially, auto-move the track line to make it follow the data series
self.trackLineEndPos = 0
Self.trackLineIsAtEnd = True
// Initially set the mouse to drag to scroll mode.
pointerPB.Value = True
// Enable mouse wheel zooming by setting the zoom ratio to 1.1 per wheel event
ChartViewer.setMouseWheelZoomRatio(1.1)
// Configure the initial viewport
ChartViewer.setViewPortWidth(initialVisibleRange / initialFullRange)
// Start the random data generator
InitRandomWalk
// Set up the chart update timer
ChartUpdateTimer.mode = timer.ModeMultiple
// The chart update rate is set to 100ms
ChartUpdateTimer.Period = 100
End Sub
Sub drawChart(viewer as ChartViewerCanvas)
// Draw chart
// Get the start date and end date that are visible on the chart.
Dim viewPortStartDate As Double = viewer.getValueAtViewPort("x", viewer.getViewPortLeft)
Dim viewPortEndDate As Double = viewer.getValueAtViewPort("x", viewer.getViewPortLeft + viewer.getViewPortWidth)
// Extract the part of the data arrays that are visible.
Dim viewPortTimeStamps() As Double
Dim viewPortDataSeriesA() As Double
Dim viewPortDataSeriesB() As Double
If (self.currentIndex > 0) Then
// Get the array indexes that corresponds to the visible start and end dates
Dim startIndex As Integer = Floor(CDXYChartMBS.bSearch(DoubleArray(self.timeStamps, self.currentIndex), viewPortStartDate))
Dim endIndex As Integer = Ceil(CDXYChartMBS.bSearch(DoubleArray(self.timeStamps, self.currentIndex), viewPortEndDate))
Dim noOfPoints As Integer = endIndex - startIndex + 1
// Extract the visible data
If (self.timeStamps(endIndex) >= viewPortStartDate) Then
viewPortTimeStamps = DoubleArray(self.timeStamps , noOfPoints, startIndex)
viewPortDataSeriesA = DoubleArray(self.dataSeriesA, noOfPoints, startIndex)
viewPortDataSeriesB = DoubleArray(self.dataSeriesB, noOfPoints, startIndex)
End If
// Keep track of the latest available data at chart plotting time
self.trackLineEndPos = self.timeStamps(self.currentIndex - 1)
End If
//
// At this stage, we have extracted the visible data. We can use those data to plot the chart.
//
//================================================================================
// Configure overall chart appearance.
//================================================================================
// Create an XYChart object of size 640 x 350 pixels
Dim c As New CDXYChartMBS(640, 350)
// 2x for higher DPI displays
c.setOutputOptions("bmpscale=2")
// Set the plotarea at (20, 30) with width 41 pixels less than chart width, and height 50 pixels
// less than chart height. Use a vertical gradient from light blue (f0f6ff) to sky blue (a0c0ff)
// as background. Set border to transparent and grid lines to white (ffffff).
Call c.setPlotArea(20, 30, c.getWidth - 41, c.getHeight - 50, c.linearGradientColor(0, 30, 0, c.getHeight - 50, &hf0f6ff, &ha0c0ff), -1, c.kTransparent, &hffffff, &hffffff)
// As the data can lie outside the plotarea in a zoomed chart, we need enable clipping.
c.setClipping
// Add a title to the chart using 18pt Arial font
Call c.addTitle(" Multithreading Real-Time Chart", "arial.ttf", 18)
// Add a legend box at (55, 25) using horizontal layout. Use 10pt Arial Bold as font. Set the
// background and border color to transparent and use line style legend key.
Dim b As CDLegendBoxMBS = c.addLegend(55, 25, False, "arialbd.ttf", 10)
b.setBackground(c.kTransparent)
b.setLineStyleKey
// Set the x and y axis stems to transparent and the label font to 10pt Arial
c.xAxis.setColors(c.kTransparent)
c.yAxis.setColors(c.kTransparent)
Call c.xAxis.setLabelStyle("arial.ttf", 10)
Call c.yAxis.setLabelStyle("arial.ttf", 10, &h336699)
// Set the y-axis tick length to 0 to disable the tick and put the labels closer to the axis.
c.yAxis.setTickLength(0)
// Add axis title using 10pt Arial Bold Italic font
Call c.yAxis.setTitle("Ionic Temperature (C)", "arialbd.ttf", 10)
// Configure the y-axis label to be inside the plot area and above the horizontal grid lines
c.yAxis.setLabelGap(-1)
c.yAxis.setLabelAlignment(1)
c.yAxis.setMargin(20)
// Configure the x-axis labels to be to the left of the vertical grid lines
c.xAxis.setLabelAlignment(1)
//================================================================================
// Add data to chart
//================================================================================
//
// In this example, we represent the data by lines. You may modify the code below to use other
// representations (areas, scatter plot, etc).
//
// Add a line layer for the lines, using a line width of 2 pixels
Dim layer As CDLineLayerMBS = c.addLineLayer
layer.setLineWidth(2)
layer.setFastLineMode
// Now we add the 2 data series to the line layer with red (ff0000) and green (00cc00) colors
layer.setXData(viewPortTimeStamps)
Call layer.addDataSet(viewPortDataSeriesA, &hff0000, "Alpha")
Call layer.addDataSet(viewPortDataSeriesB, &h00cc00, "Beta")
//================================================================================
// Configure axis scale and labelling
//================================================================================
// Set the x-axis as a date/time axis with the scale according to the view port x range.
If (self.currentIndex > 0) Then
c.xAxis.setDateScale(viewPortStartDate, viewPortEndDate, 5.0)
End If
// For the automatic axis labels, set the minimum spacing to 75/30 pixels for the x/y axis.
c.xAxis.setTickDensity(75)
c.yAxis.setTickDensity(30)
// We use "hh:nn:ss" as the axis label format.
c.xAxis.setLabelFormat("{value|hh:nn:ss}")
// We make sure the tick increment must be at least 1 second.
c.xAxis.setMinTickInc(1)
// Set the auto-scale margin to 0.05, and the zero affinity to 0.6
c.yAxis.setAutoScale(0.05, 0.05, 0.6)
//================================================================================
// Output the chart
//================================================================================
// We need to update the track line too. If the mouse is moving on the chart (eg. if
// the user drags the mouse on the chart to scroll it), the track line will be updated
// in the MouseMovePlotArea event. Otherwise, we need to update the track line here.
If (Not ChartViewer.isInMouseMoveEvent) Then
Dim x As Integer
If self.trackLineIsAtEnd Then
x = c.getWidth
Else
x = ChartViewer.getPlotAreaMouseX
End If
Call trackLineLabel(c, x)
End If
// Set the chart image to the QChartViewer
viewer.setChart(c)
End Sub
Sub drawFullChart(vpc as ViewPortControl)
// Draw the full thumbnail chart and display it in the given QViewPortControl
// Create an XYChart object of size 640 x 60 pixels
Dim c As New CDXYChartMBS(640, 60)
// 2x for higher DPI displays
c.setOutputOptions("bmpscale=2")
// Set the plotarea with the same horizontal position as that in the main chart for alignment.
Call c.setPlotArea(20, 0, c.getWidth - 41, c.getHeight - 1, &hc0d8ff, -1, &hcccccc, c.kTransparent, &hffffff)
// Set the x axis stem to transparent and the label font to 10pt Arial
c.xAxis.setColors(c.kTransparent)
Call c.xAxis.setLabelStyle("Arial", 10)
// Put the x-axis labels inside the plot area by setting a negative label gap. Use
// setLabelAlignment to put the label at the right side of the tick.
c.xAxis.setLabelGap(-1)
c.xAxis.setLabelAlignment(1)
// Set the y axis stem and labels to transparent (that is, hide the labels)
c.yAxis.setColors(c.kTransparent, c.kTransparent)
// Add a line layer for the lines with fast line mode enabled
Dim layer As CDLineLayerMBS = c.addLineLayer
layer.setFastLineMode
// Now we add the 3 data series to a line layer, using the color red (&hff3333), green
// (&h008800) and blue (&h3333cc)
layer.setXData( DoubleArray(self.timeStamps, self.currentIndex) )
Call layer.addDataSet(DoubleArray(self.dataSeriesA, self.currentIndex), &hff3333)
Call layer.addDataSet(DoubleArray(Self.dataSeriesB, Self.currentIndex), &h008800)
// The x axis scales should reflect the full range of the view port
Dim lowerLimit As Double = vpc.getViewer.getValueAtViewPort("x", 0)
Dim higherLimit As Double = vpc.getViewer.getValueAtViewPort("x", 1)
c.xAxis.setDateScale(lowerLimit, higherLimit, 10)
c.xAxis.setLabelFormat("{value|nn:ss}")
// For the automatic x-axis labels, set the minimum spacing to 75 pixels.
c.xAxis.setTickDensity(75)
// For the auto-scaled y-axis, as we hide the labels, we can disable axis rounding. This can
// make the axis scale fit the data tighter.
c.yAxis.setRounding(False, False)
// Output the chart
vpc.SetChart c
End Sub
Sub onChartUpdateTimer()
// Get data from the queue, update the viewport and update the chart display if necessary.
Dim viewer As ChartViewerCanvas = Self.ChartViewer
// Enables auto scroll if the viewport is showing the latest data before the update
Dim autoScroll As Boolean = False
If (Self.currentIndex > 0) Then
Dim value As Double = viewer.getValueAtViewPort("x", viewer.getViewPortLeft + viewer.getViewPortWidth)
Dim LastTimestamp As Double = Self.timeStamps(Self.currentIndex - 1)
autoScroll = (0.001 + value >= LastTimestamp)
End If
//
// Get new data from the queue and append them to the data arrays
//
Dim NewQueue() As DataPacket
// take old queue
Dim Packets() As DataPacket = Queue
// and put new array there instead
Queue = NewQueue
// now did we get data?
Dim count As Integer = Packets.Ubound + 1
If count <= 0 Then
Return
End If
// if data arrays have insufficient space, we need to remove some old data.
If (Self.currentIndex + count >= sampleSize) Then
// For safety, we check if the queue contains too much data than the entire data arrays. If
// this is the case, we only use the latest data to completely fill the data arrays.
While (count > sampleSize)
packets.Remove(0)
count = count -1
Wend
// Remove oldest data to leave space for new data. To avoid frequent removal, we ensure at
// least 5% empty space available after removal.
Dim originalIndex As Integer = Self.currentIndex
Self.currentIndex = sampleSize * 95 / 100 - 1
If (Self.currentIndex > sampleSize - count) Then
Self.currentIndex = sampleSize - count
End If
For i As Integer = 0 To Self.currentIndex - 1
Dim srcIndex As Integer = i + originalIndex - Self.currentIndex
Self.timeStamps(i) = Self.timeStamps(srcIndex)
Self.dataSeriesA(i) = Self.dataSeriesA(srcIndex)
Self.dataSeriesB(i) = Self.dataSeriesB(srcIndex)
Next
End If
// Append the data from the queue to the data arrays
For n As Integer = 0 To count-1
self.timeStamps(self.currentIndex) = packets(n).elapsedTime
self.dataSeriesA(self.currentIndex) = packets(n).series0
self.dataSeriesB(self.currentIndex) = packets(n).series1
self.currentIndex = self.currentIndex + 1
Next
//
// As we added more data, we may need to update the full range of the viewport.
//
Dim startDate As Double = self.timeStamps(0)
Dim endDate As Double = self.timeStamps(self.currentIndex - 1)
// Use the initialFullRange (which is 60 seconds in this demo) if this is sufficient.
Dim duration As Double = endDate - startDate
If (duration < initialFullRange) Then
endDate = startDate + initialFullRange
End If
// Update the new full data range to include the latest data
Const KeepVisibleRange = 1
Dim axisScaleHasChanged As Boolean = viewer.updateFullRangeH("x", startDate, endDate, KeepVisibleRange)
If (autoScroll) Then
// Scroll the viewport if necessary to display the latest data
Dim viewPortEndPos As Double = viewer.getViewPortAtValue("x", Self.timeStamps(Self.currentIndex - 1))
Dim ViewPortLeft As Double = viewer.getViewPortLeft
Dim ViewPortWidth As Double = viewer.getViewPortWidth
If (viewPortEndPos > ViewPortLeft + ViewPortWidth) Then
viewer.setViewPortLeft(viewPortEndPos - ViewPortWidth)
axisScaleHasChanged = true
End If
end if
// Set the zoom in limit as a ratio to the full range
viewer.setZoomInWidthLimit(zoomInLimit / (viewer.getValueAtViewPort("x", 1) - viewer.getValueAtViewPort("x", 0)))
// Trigger the viewPortChanged event. Updates the chart if the axis scale has changed
// (scrolling or zooming) or if new data are added to the existing axis scale.
viewer.updateViewPort(axisScaleHasChanged Or (duration < initialFullRange), False)
End Sub
Sub onData(r As RandomWalk, currentTime As Double, value1 As Double, value2 As Double)
//
// Handles realtime data from RandomWalk. The RandomWalk will call this method from its own thread.
// This is connect to an event
//
Dim p As New DataPacket
p.elapsedTime = currentTime
p.series0 = value1
p.series1 = value2
Queue.Append p
#Pragma unused r
End Sub
Sub onMouseMovePlotArea(MouseEvent as MouseEvent)
// Draw track cursor when mouse is moving over plotarea
Dim BaseChart As CDBaseChartMBS = ChartViewer.currentChart
Dim Chart As CDXYChartMBS = CDXYChartMBS(BaseChart)
Dim trackLinePos As Double = trackLineLabel(Chart, ChartViewer.getPlotAreaMouseX)
self.trackLineIsAtEnd = (self.currentIndex <= 0) or (trackLinePos = self.trackLineEndPos)
ChartViewer.updateDisplay
#Pragma unused MouseEvent
End Sub
Sub onMouseUsageChanged(mouseUsage as integer)
// Pointer/zoom in/zoom out button clicked
ChartViewer.setMouseUsage(mouseUsage)
End Sub
Sub onSave()
Dim p As Picture = ChartViewer.GetPicture
If p <> Nil Then
Dim f As FolderItem = GetSaveFolderItem(FileTypes1.Png, "chart.png")
If f <> Nil Then
p.Save(f, Picture.SaveAsPNG)
End If
End If
End Sub
Sub onViewPortChanged()
// View port changed event
// Update the chart if necessary
If (ChartViewer.needUpdateChart) Then
drawChart(ChartViewer)
End If
// Update the full chart
drawFullChart(ViewPortControl)
End Sub
Function trackLineLabel(c as CDXYChartMBS, mouseX as Integer) As double
// Draw the track line with data point labels
// Clear the current dynamic layer and get the DrawArea object to draw on it.
Dim d As CDDrawAreaMBS = c.initDynamicLayer
// The plot area object
Dim plotArea As CDPlotAreaMBS = c.getPlotArea
// Get the data x-value that is nearest to the mouse, and find its pixel coordinate.
dim xValue as double = c.getNearestXValue(mouseX)
Dim xCoor As Integer = c.getXCoor(xValue)
If (xCoor < plotArea.getLeftX) then
Return xValue
end if
// Draw a vertical track line at the x-position
d.vline(plotArea.getTopY, plotArea.getBottomY, xCoor, &h888888)
// Draw a label on the x-axis to show the track line position.
Dim xlabel As String
xlabel = "<*font,bgColor=000000*> "
xlabel = xlabel + c.xAxis.getFormattedLabel(xValue + 0.00499, "hh:nn:ss.ff")
xlabel = xlabel + " <*/font*>"
Dim t As CDTTFTextMBS = d.Text(xlabel, "arialbd.ttf", 10)
// Restrict the x-pixel position of the label to make sure it stays inside the chart image.
Dim xLabelPos As Integer = Max(0, Min(xCoor - t.getWidth / 2, c.getWidth - t.getWidth))
t.draw(xLabelPos, plotArea.getBottomY + 2, &hffffff)
t.destroy
// Iterate through all layers to draw the data labels
For i As Integer = 0 To c.getLayerCount-1
Dim layer As CDLayerMBS = c.getLayerByZ(i)
// The data array index of the x-value
Dim xIndex As Integer = layer.getXIndexOf(xValue)
// Iterate through all the data sets in the layer
For j As Integer = 0 To layer.getDataSetCount-1
Dim dataSet As CDDataSetMBS = layer.getDataSetByZ(j)
Dim dataSetName As String = dataSet.getDataName
// Get the color, name and position of the data label
Dim ColorValue As Integer = dataSet.getDataColor
Dim yCoor As Integer = c.getYCoor(dataSet.getPosition(xIndex), dataSet.getUseYAxis)
// Draw a track dot with a label next to it for visible data points in the plot area
If ((yCoor >= plotArea.getTopY) And (yCoor <= plotArea.getBottomY) And (ColorValue <> c.kTransparent) And dataSetName.Len > 0) Then
d.circle(xCoor, yCoor, 4, 4, ColorValue, ColorValue)
Dim label as string
label = "<*font,bgColor="
label = label + Right("000000"+Hex(ColorValue), 6)
label = label + "*> "
label = label + c.formatValue(dataSet.getValue(xIndex), "{value|P4}")
label = label + " <*font*>"
t = d.Text(label, "arialbd.ttf", 10)
// Draw the label on the right side of the dot if the mouse is on the left side the
// chart, and vice versa. This ensures the label will not go outside the chart image.
If (xCoor <= (plotArea.getLeftX + plotArea.getRightX) / 2) Then
t.draw(xCoor + 6, yCoor, &hffffff, c.kLeft)
Else
t.draw(xCoor - 6, yCoor, &hffffff, c.kRight)
End If
t.destroy
end if
next
Next
Return xValue
End Function
Property Queue() As DataPacket
Property ViewPortControl As ViewPortControl
Property currentIndex As Integer
Property dataSeriesA() As double
Property dataSeriesB() As double
Property dataSource As RandomWalk
Property fullChart As CDXYChartMBS
Property timeStamps() As Double
Property trackLineEndPos As double
If the track cursor Is at the End Of the data series, we will automatic move the track line when new data arrives.
Property trackLineIsAtEnd As Boolean
If the track cursor Is at the End Of the data series, we will automatic move the track line when new data arrives.
End Class
MenuBar MainMenuBar
MenuItem FileMenu = "&File"
MenuItem FileQuit = "#App.kFileQuit"
MenuItem EditMenu = "&Edit"
MenuItem EditUndo = "&Undo"
MenuItem EditSeparator1 = "-"
MenuItem EditCut = "Cu&t"
MenuItem EditCopy = "&Copy"
MenuItem EditPaste = "&Paste"
MenuItem EditClear = "#App.kEditClear"
MenuItem EditSeparator2 = "-"
MenuItem EditSelectAll = "Select &All"
End MenuBar
Class RandomWalk Inherits Thread
Event GotNewValues(currentTime as double, value1 as double, value2 as Double)
End Event
EventHandler Sub Run()
Dim currentTime As Int64 = 0
Dim nextTime As Int64 = 0
// Random walk variables
Dim series0 As Double = 32
Dim series1 As Double = 63
Dim upperLimit As Double = 94
Dim scaleFactor As Double = Sqrt(interval * 0.3)
Dim r As New Random
// Variables to keep track of the timing
Dim startTime As Int64 = Microseconds
While Not stoppedThread
// Compute the next data value
currentTime = Microseconds - startTime
series0 = Abs(series0 + (r.Number - 0.5) * scaleFactor)
If (series0 > upperLimit) Then
series0 = upperLimit * 2 - series0
End If
series1 = Abs(series1 + (r.Number - 0.5) * scaleFactor)
If (series1 > upperLimit) Then
series1 = upperLimit * 2 - series1
end if
// Call the handler
Dim milliseconds As Double = currentTime / 1000.0
Dim seconds As Double = milliseconds / 1000.0
RaiseEvent GotNewValues(seconds, series0, series1)
'System.DebugLog Str(milliseconds, "0.0")+" "+Str(series0)+" "+Str(series1)
// Sleep until next walk
nextTime = nextTime + interval
If (nextTime <= currentTime) Then
nextTime = currentTime + interval
End If
Dim delta As Int64 = (nextTime - currentTime)
Me.Sleep delta
Wend
End EventHandler
Sub stopThread()
stoppedThread = True
app.YieldToNextThread
// wait for ending
While Me.State = Me.Running
app.YieldToNextThread
Wend
stoppedThread = False
End Sub
Property interval As Integer = 100
// The period of the data series in milliseconds. This random series implementation just use the
// windows timer for timing. In many computers, the default windows timer resolution is 1/64 sec,
// or 15.6ms. This means the interval may not be exactly accurate.
Property stoppedThread As Boolean
End Class
Class DataPacket
Property elapsedTime As double
Property series0 As double
Property series1 As double
End Class
Class ChartViewerCanvas Inherits ViewPortManagerCanvas
Const CLOCKS_PER_SEC = 1000
Const MouseUsageDefault = 0
Const MouseUsageScroll = 1
Const MouseUsageZoomIn = 3
Const MouseUsageZoomOut = 4
Const NEED_DELAY = 1
Const NEED_UPDATE = 2
Const NO_DELAY = 0
Const UNDEFINED_COOR = -1073741823
Event Open()
End Event
Event clicked(MouseEvent as MouseEvent)
End Event
Event mouseLeaveChart(MouseEvent as MouseEvent)
End Event
Event mouseLeavePlotArea(MouseEvent as MouseEvent)
End Event
Event mouseMove(MouseEvent as MouseEvent)
End Event
Event mouseMoveChart(MouseEvent as MouseEvent)
End Event
Event mouseMovePlotArea(MouseEvent as MouseEvent)
End Event
Event mouseWheel(MouseEvent as MouseEvent)
End Event
Event viewPortChanged()
End Event
EventHandler Function MouseDown(X As Integer, Y As Integer) As Boolean
Dim m As New MouseEvent(x,y)
m.SetButton
mousePressEvent(m)
Return True
End EventHandler
EventHandler Sub MouseDrag(X As Integer, Y As Integer)
Dim m As New MouseEvent(x,y)
m.SetButton
mouseMoveEvent m
End EventHandler
EventHandler Sub MouseEnter()
'Dim m As New MouseEvent(Self.Mousex, Self.MouseY)
End EventHandler
EventHandler Sub MouseExit()
Dim m As New MouseEvent(Self.Mousex, Self.MouseY)
leaveEvent m
End EventHandler
EventHandler Sub MouseMove(X As Integer, Y As Integer)
Dim m As New MouseEvent(x,y)
mouseMoveEvent m
End EventHandler
EventHandler Sub MouseUp(X As Integer, Y As Integer)
Dim m As New MouseEvent(x,y)
m.SetButton
mouseReleaseEvent(m)
End EventHandler
EventHandler Function MouseWheel(X As Integer, Y As Integer, deltaX as Integer, deltaY as Integer) As Boolean
Dim m As New MouseEvent(x,y)
m.delta = deltay
wheelEvent(m)
#Pragma Unused deltaX
End EventHandler
EventHandler Sub Open()
ViewPortManager = New CDViewPortManagerMBS
// current chart and hot spot tester
Self.currentChart = Nil
hotSpotTester = nil
// initialize chart configuration
selectBoxLineColor = &c000000
selectBoxLineWidth = 2
mouseUsage = ChartViewerCanvas.MouseUsageDefault
zoomDirection = CDBaseChartMBS.kDirectionHorizontal
zoomInRatio = 2
zoomOutRatio = 0.5
mouseWheelZoomRatio = 1
scrollDirection = CDBaseChartMBS.kDirectionHorizontal
minDragAmount = 5
updateInterval = 20
// current state of the mouse
isOnPlotArea = false
isPlotAreaMouseDown = false
isDragScrolling = false
currentHotSpot = -1
isClickable = false
isMouseTracking = false
isInMouseMove = false
// chart update rate support
needUpdateChart = False
needUpdateImageMap = False
holdTimerActive = False
isInViewPortChanged = false
delayUpdateChart = NO_DELAY
delayedChart = nil
lastMouseMove = 0
delayedMouseEvent = nil
delayImageMapUpdate = false
// track cursor support
autoHideMsg = ""
currentMouseX = UNDEFINED_COOR
currentMouseY = UNDEFINED_COOR
isInMouseMovePlotArea = false
// selection rectangle
'LeftLine = 0
'RightLine = 0
'TopLine = 0
'BottomLine = 0
// xojo does it always
'setMouseTracking(True)
RaiseEvent Open
End EventHandler
EventHandler Sub Paint(g As Graphics, areas() As REALbasic.Rect)
If ChartPicture <> Nil Then
g.DrawPicture ChartPicture, 0, 0, g.Width, g.height, 0, 0, ChartPicture.Width, ChartPicture.Height
End If
#Pragma Unused areas
End EventHandler
Sub Constructor()
// Calling the overridden superclass constructor.
Super.Constructor
End Sub
Sub SetChart(c as CDBaseChartMBS)
// Set the chart to the control
Self.currentChart = c
Dim map As String ' = c.getHTMLImageMap("test")
Self.setImageMap(map)
If c <> Nil Then
commitPendingSyncAxis(c)
If delayUpdateChart <> NO_DELAY Then
// render chart now
Call c.makeChart
End If
End If
Self.updateDisplay
End Sub
Sub applyAutoHide(msg as string)
// Attempt to hide the dynamic layer using the specified message
If (autoHideMsg = msg) then
If (Nil <> currentChart) Then
currentChart.removeDynamicLayer
End If
autoHideMsg = ""
updateDisplay
End If
End Sub
Function clock() As double
// in milliseconds
Return Microseconds / 1000.0
End Function
Sub commitMouseMove(MouseEvent as MouseEvent)
// Remember the mouse coordinates for later use
Self.currentMouseX = MouseEvent.x
Self.currentMouseY = MouseEvent.y
// The chart can be updated more than once during mouse move. For example, it can update due to
// drag to scroll, and also due to drawing track cursor. So we delay updating the display until
// all all events has occured.
Self.delayUpdateChart = NEED_DELAY
Self.isInMouseMove = True
// Check if mouse is dragging on the plot area
Self.isOnPlotArea = Self.isPlotAreaMouseDown Or inPlotArea(MouseEvent.x, MouseEvent.y)
If (Self.isPlotAreaMouseDown) Then
onPlotAreaMouseDrag(MouseEvent)
End If
// Emit mouseMoveChart
RaiseEvent mouseMoveChart(MouseEvent)
If (inExtendedPlotArea(MouseEvent.x, MouseEvent.y)) Then
// Mouse is in extended plot area, emit mouseMovePlotArea
Self.isInMouseMovePlotArea = True
RaiseEvent mouseMovePlotArea(MouseEvent)
Elseif (Self.isInMouseMovePlotArea) Then
// Mouse was in extended plot area, but is not in it now, so emit mouseLeavePlotArea
Self.isInMouseMovePlotArea = False
RaiseEvent mouseLeavePlotArea(MouseEvent)
applyAutoHide("mouseleaveplotarea")
end if
//
// Show hot spot tool tips if necessary
// (Due to QT bug, tooltip cannot be put in delayedMouseEvent, otherwise sometimes tooltip
// position will be incorrect.)
//
If (Self.delayImageMapUpdate) Then
Self.delayImageMapUpdate = False
If (Not Self.isPlotAreaMouseDown) Then
updateViewPort(False, True)
End If
End If
If (MouseEvent.button <> MouseEvent.NoButton) Then
// Hide tool tips if mouse button is pressed.
Self.HelpTag = ""
If HelpLabel <> Nil Then
HelpLabel.Text = ""
End If
Else
// Use the ChartDirector ImageMapHandler to determine if the mouse is over a hot spot
dim hotSpotNo as integer = 0
If (Nil <> Self.hotSpotTester) Then
hotSpotNo = Self.hotSpotTester.getHotSpot(MouseEvent.x, MouseEvent.y)
End If
// If the mouse is in the same hot spot since the last mouse move event, there is no need
// to update the tool tip.
If (hotSpotNo <> Self.currentHotSpot) Then
// Hot spot has changed - update tool tip text
Self.currentHotSpot = hotSpotNo
If (hotSpotNo = 0) Then
// Mouse is not actually on hanlder hot spot - use default tool tip text and reset
// the clickable flag.
Self.HelpTag = Self.defaultToolTip
Self.isClickable = False
If HelpLabel <> Nil Then
HelpLabel.text = self.defaultToolTip
End If
Else
// Mouse is on a hot spot. In this implementation, we consider the hot spot as
// clickable if its href ("path") parameter is not empty.
Dim path As String = Self.hotSpotTester.getValue("path")
Self.isClickable = ("" <> path)
Dim s As String = Self.hotSpotTester.getValue("title")
Self.HelpTag = s
If HelpLabel <> Nil Then
HelpLabel.Text = s
End If
End If
End If
End If
// Cancel the delayed mouse event if any
If (Self.delayedMouseEvent <> Nil) Then
killTimer(delayedMouseEventTimerId)
Self.delayedMouseEvent = nil
End If
// Can update chart now
commitUpdateChart
Self.isInMouseMove = False
Self.lastMouseMove = clock
End Sub
Sub commitUpdateChart()
// Commit chart to display
If (Self.delayUpdateChart = NEED_DELAY) Then
// No actual update occur
Self.delayUpdateChart = NO_DELAY
Return
end if
// Get the image and metrics for the chart
Dim c As CDBaseChartMBS
If delayUpdateChart = NEED_UPDATE Then
c = Self.delayedChart
Else
c = Self.currentChart
end if
Dim ChartMetrics As String
dim image as Picture
If c <> Nil Then
// Output chart as Device Indpendent Bitmap with file headers
image = c.makeChartPicture
// Get chart metrics
ChartMetrics = c.getChartMetrics
end if
// Set the QPixmap for display
Self.ChartPicture = image
Self.Invalidate
// Set the chart metrics and clear the image map
setChartMetrics(ChartMetrics)
// Any delayed chart has been committed
self.delayUpdateChart = NO_DELAY
Self.delayedChart = Nil
End Sub
Sub drawRect(x as integer, y as integer, width as integer, height as integer)
// Create the edges of the rectangle if not already created
// width < 0 is interpreted as the rectangle extends to the left or x.
// height <0 is interpreted as the rectangle extends to above y.
If (width < 0) Then
width = -width
x = x - width
End If
If (height < 0) Then
height = -height
y = y - height
End If
// Put the edges along the sides of the rectangle
TopLine.X1 = Me.Left + x
TopLine.Y1 = Me.top + y
TopLine.X2 = TopLine.X1 + width
TopLine.Y2 = TopLine.Y1
TopLine.LineColor = selectBoxLineColor
TopLine.BorderWidth = selectBoxLineWidth
LeftLine.X1 = Me.Left + x
LeftLine.Y1 = Me.top + y
LeftLine.X2 = LeftLine.X1
LeftLine.Y2 = LeftLine.Y1 + height
LeftLine.LineColor = selectBoxLineColor
LeftLine.BorderWidth = selectBoxLineWidth
BottomLine.X1 = Me.Left + x
BottomLine.Y1 = Me.top + y + height - selectBoxLineWidth + 1
BottomLine.X2 = BottomLine.X1 + width
BottomLine.Y2 = BottomLine.Y1
BottomLine.LineColor = selectBoxLineColor
BottomLine.BorderWidth = selectBoxLineWidth
RightLine.X1 = Me.Left + x + width - selectBoxLineWidth + 1
RightLine.Y1 = Me.top + y
RightLine.X2 = RightLine.X1
RightLine.Y2 = RightLine.Y1 + height
RightLine.LineColor = selectBoxLineColor
RightLine.BorderWidth = selectBoxLineWidth
End Sub
Function getChart() As CDBaseChartMBS
// Get back the same BaseChart pointer provided by the previous setChart call.
Return self.currentChart
End Function
Function getChartMouseX() As integer
// Get the current mouse x coordinate when used in a mouse move event handler
Dim ret As Integer = currentMouseX
If (ret = UNDEFINED_COOR) then
ret = self.getPlotAreaLeft + self.getPlotAreaWidth
End If
Return ret
End Function
Function getChartMouseY() As integer
// Get the current mouse y coordinate when used in a mouse move event handler
Dim ret As Integer = currentMouseY
If ret = UNDEFINED_COOR then
ret = Self.getPlotAreaTop + Self.getPlotAreaHeight
End If
Return ret
End Function
Function getImageMapHandler() As CDImageMapHandlerMBS
// Get the image map handler for the chart
Return hotSpotTester
End Function
Function getMinimumDrag() As Integer
// Get the minimum mouse drag before the dragging is considered as real.
Return minDragAmount
End Function
Function getMouseUsage() As Integer
Return mouseUsage
End Function
Function getMouseWheelZoomRatio() As double
// Get the mouse wheel zoom ratio
Return mouseWheelZoomRatio
End Function
Function getPicture() As Picture
If ChartPicture <> Nil Then
Return ChartPicture
Else
// create if needed
Return currentChart.makeChartPicture
End If
End Function
Function getPlotAreaMouseX() As integer
// Get the current mouse x coordinate bounded to the plot area when used in a mouse event handler
Dim ret As Integer = getChartMouseX
If (ret < getPlotAreaLeft) then
ret = getPlotAreaLeft
End If
If (ret > getPlotAreaLeft + getPlotAreaWidth) Then
ret = getPlotAreaLeft + getPlotAreaWidth
End If
Return ret
End Function
Function getPlotAreaMouseY() As integer
// Get the current mouse y coordinate bounded to the plot area when used in a mouse event handler
Dim ret As Integer = getChartMouseY
If (ret < getPlotAreaTop) Then
ret = getPlotAreaTop
End If
If (ret > getPlotAreaTop + getPlotAreaHeight) Then
ret = getPlotAreaTop + getPlotAreaHeight
End If
Return ret
End Function
Function getScrollDirection() As Integer
// Get the scroll direction
Return scrollDirection
End Function
Function getSelectionBorderColor() As color
// Get the border color of the selection box.
Return selectBoxLineColor
End Function
Function getSelectionBorderWidth() As Integer
Return selectBoxLineWidth
End Function
Function getUpdateInterval() As Integer
// Get the minimum interval between ViewPortChanged events.
Return Self.updateInterval
End Function
Function getZoomDirection() As Integer
// Get the zoom direction
Return zoomDirection
End Function
Function getZoomInRatio() As double
// Get the zoom-in ratio for mouse click zoom-in
//
Return zoomInRatio
End Function
Function getZoomOutRatio() As Double
// Get the zoom-out ratio
Return zoomOutRatio
End Function
Sub initRect()
// Create the edges for the selection rectangle
'LeftLine = New QLabel(this);
'LeftLine.setAutoFillBackground(True);
'RightLine = New QLabel(this);
'RightLine.setAutoFillBackground(True);
'TopLine = New QLabel(this);
'TopLine.setAutoFillBackground(True);
'BottomLine = New QLabel(this);
'BottomLine.setAutoFillBackground(True);
setSelectionBorderColor(getSelectionBorderColor)
End Sub
Function isDrag(direction as integer, MouseEvent as MouseEvent) As Boolean
// Determines if the mouse is dragging.
// We only consider the mouse is dragging it is has dragged more than self.minDragAmount. This is
// to avoid small mouse vibrations triggering a mouse drag.
Dim spanX As Integer = Abs(MouseEvent.X - self.plotAreaMouseDownXPos)
Dim spanY As Integer = Abs(MouseEvent.Y - Self.plotAreaMouseDownYPos)
Return _
((direction <> CDBaseChartMBS.kDirectionVertical ) And (spanX >= minDragAmount)) Or _
((direction <> CDBaseChartMBS.kDirectionHorizontal) And (spanY >= minDragAmount))
End Function
Function isInMouseMoveEvent() As Boolean
// Check if is currently processing a mouse move event
Return self.isInMouseMove
End Function
Function isInViewPortChangedEvent() As Boolean
// Check if is currently processing a view port changed event
Return isInViewPortChanged
End Function
Function isMouseDragging() As Boolean
// Check if mouse is dragging to scroll or to select the zoom rectangle
Return isPlotAreaMouseDown
End Function
Function isMouseOnPlotArea() As Boolean
// Check if mouse is on the extended plot area
If (isMouseTracking) Then
Return inExtendedPlotArea(getChartMouseX, getChartMouseY)
Else
Return False
End If
End Function
Sub killTimer(byref t as timer)
If t <> Nil Then
t.Mode = timer.ModeOff
t = Nil
End If
End Sub
Sub leaveEvent(MouseEvent as MouseEvent)
// Process delayed mouse move, if any
onDelayedMouseMove
// Mouse tracking is no longer active
Self.isMouseTracking = false
If (self.isInMouseMovePlotArea) then
// Mouse was in extended plot area, but is not in it now, so emit mouseLeavePlotArea
self.isInMouseMovePlotArea = false
RaiseEvent mouseLeavePlotArea(MouseEvent)
applyAutoHide("mouseleaveplotarea")
End If
// emit mouseLeaveChart
RaiseEvent mouseLeaveChart(MouseEvent)
applyAutoHide("mouseleavechart")
End Sub
Sub mouseMoveEvent(MouseEvent as MouseEvent)
// Enable mouse tracking to detect mouse leave events
Self.isMouseTracking = True
RaiseEvent mouseMove(MouseEvent)
// On Windows, mouse events can by-pass the event queue. If there are too many mouse events,
// the event queue may not get processed, preventing other controls from updating. If two mouse
// events are less than 10ms apart, there is a risk of too many mouse events. So we repost the
// mouse event as a timer event that is queued up normally, allowing the queue to get processed.
Dim timeBetweenMouseMove As Integer = ((clock) - Self.lastMouseMove) * 1000 / CLOCKS_PER_SEC
If ((Self.delayedMouseEvent <> Nil And (timeBetweenMouseMove < 250)) Or (timeBetweenMouseMove < 10)) Then
If (Nil = Self.delayedMouseEvent) then
Self.delayedMouseEventTimerId = startTimer(1)
Else
Self.delayedMouseEvent = Nil
End If
Self.delayedMouseEvent = MouseEvent
Else
commitMouseMove(MouseEvent)
End If
onSetCursor
End Sub
Sub mousePressEvent(MouseEvent as MouseEvent)
// Mouse button down event.
onDelayedMouseMove
If ((MouseEvent.button = MouseEvent.LeftButton) And inPlotArea(MouseEvent.x, MouseEvent.y) And (mouseUsage <> MouseUsageDefault)) Then
// Mouse usage is for drag to zoom/scroll. Capture the mouse to prepare for dragging and
// save the mouse down position to draw the selection rectangle.
Self.isPlotAreaMouseDown = True
Self.plotAreaMouseDownXPos = MouseEvent.x
Self.plotAreaMouseDownYPos = MouseEvent.y
startDrag
End If
End Sub
Sub mouseReleaseEvent(MouseEvent as MouseEvent)
// Mouse button up event.
onDelayedMouseMove
If ((MouseEvent.button = MouseEvent.LeftButton) and self.isPlotAreaMouseDown) then
// Release the mouse capture.
self.isPlotAreaMouseDown = false
setRectVisible(False)
Dim hasUpdate As Boolean = False
Select Case Self.mouseUsage
Case MouseUsageZoomIn
If (canZoomIn(Self.zoomDirection)) Then
If (isDrag(Self.zoomDirection, MouseEvent)) Then
// Zoom to the drag selection rectangle.
hasUpdate = zoomTo(Self.zoomDirection, Self.plotAreaMouseDownXPos, Self.plotAreaMouseDownYPos, MouseEvent.x, MouseEvent.y)
Else
// User just click on a point. Zoom-in around the mouse cursor position.
hasUpdate = zoomAt(Self.zoomDirection, MouseEvent.x, MouseEvent.y, Self.zoomInRatio)
end if
End If
Case MouseUsageZoomOut
// Zoom out around the mouse cursor position.
If (canZoomOut(Self.zoomDirection)) Then
hasUpdate = zoomAt(Self.zoomDirection, MouseEvent.x, MouseEvent.y, Self.zoomOutRatio)
End If
Else
If (Self.isDragScrolling) Then
// Drag to scroll. We can update the image map now as scrolling has finished.
updateViewPort(False, True)
Else
// Is not zooming or scrolling, so is just a normal click event.
RaiseEvent clicked(MouseEvent)
end if
End Select
self.isDragScrolling = false
If (hasUpdate) then
// View port has changed - update it.
updateViewPort(True, True)
End If
Else
RaiseEvent clicked(MouseEvent)
End If
onSetCursor
End Sub
Sub onDelayedMouseMove()
// Delayed MouseMove event handler
If (delayedMouseEvent <> nil) Then
commitMouseMove(Self.delayedMouseEvent)
End If
End Sub
Function onMouseWheelZoom(x as integer, y as integer, zDelta as integer) As Boolean
// Handles mouse wheel zooming
// Zoom ratio = 1 means no zooming
If (Self.mouseWheelZoomRatio = 1) Then
Return False
End If
// X and Y zoom ratios
Dim rx As Double = 1
Dim ry As Double = 1
If (getZoomDirection <> CDBaseChartMBS.kDirectionVertical) Then
If zDelta = 0 Then
elseIf (zDelta > 0) Then
rx = Self.mouseWheelZoomRatio
Else
rx = 1.0 / Self.mouseWheelZoomRatio
end if
End If
If (getZoomDirection <> CDBaseChartMBS.kDirectionHorizontal) Then
If zDelta = 0 Then
elseIf (zDelta > 0) Then
ry = Self.mouseWheelZoomRatio
Else
ry = 1.0 / Self.mouseWheelZoomRatio
End If
End If
// Do the zooming
If (zoomAround(x, y, rx, ry)) Then
updateViewPort(True, True)
End If
Return True
End Function
Sub onPlotAreaMouseDrag(MouseEvent as MouseEvent)
Select Case mouseUsage
Case MouseUsageZoomIn
//
// Mouse is used for zoom in. Draw the selection rectangle if necessary.
//
Dim isDragZoom As Boolean = canZoomIn(zoomDirection) And isDrag(zoomDirection, MouseEvent)
If (isDragZoom) Then
Dim spanX As Integer = plotAreaMouseDownXPos - MouseEvent.x
Dim spanY As Integer = plotAreaMouseDownYPos - MouseEvent.y
Select Case zoomDirection
Case CDBaseChartMBS.kDirectionHorizontal
drawRect(MouseEvent.x, getPlotAreaTop, spanX, getPlotAreaHeight)
Case CDBaseChartMBS.kDirectionVertical
drawRect(getPlotAreaLeft, MouseEvent.y, getPlotAreaWidth, spanY)
Else
drawRect(MouseEvent.x, MouseEvent.y, spanX, spanY)
End Select
End If
setRectVisible(isDragZoom)
Case MouseUsageScroll
//
// Mouse is used for drag scrolling. Scroll and update the view port.
//
If (isDragScrolling Or isDrag(scrollDirection, MouseEvent)) Then
isDragScrolling = True
If (dragTo(scrollDirection, MouseEvent.x - plotAreaMouseDownXPos, MouseEvent.y - plotAreaMouseDownYPos)) then
updateViewPort(True, False)
End If
end if
End Select
End Sub
Sub onSetCursor()
If isDragScrolling Then
Select Case scrollDirection
Case CDBaseChartMBS.kDirectionHorizontal
Me.MouseCursor = System.Cursors.ArrowEastWest
Case CDBaseChartMBS.kDirectionVertical
Me.MouseCursor = System.Cursors.ArrowNorthSouth
Else
Me.MouseCursor = System.Cursors.StandardPointer
End Select
Return
End If
If isOnPlotArea then
select case mouseUsage
Case MouseUsageZoomIn
If (canZoomIn(zoomDirection)) Then
Me.MouseCursor = System.Cursors.MagnifyLarger
Else
Me.MouseCursor = System.Cursors.FingerPointer
End If
Return
Case MouseUsageZoomOut
If (canZoomOut(zoomDirection)) Then
Me.MouseCursor = System.Cursors.MagnifySmaller
Else
Me.MouseCursor = System.Cursors.FingerPointer
End If
Return
end Select
End If
If isClickable Then
Me.MouseCursor = System.Cursors.FingerPointer
Else
Me.MouseCursor = Nil
End If
End Sub
Sub removeDynamicLayer(msg as string)
// Set the message used to remove the dynamic layer
autoHideMsg = msg.Lowercase
If autoHideMsg = "now" then
applyAutoHide(msg)
End If
End Sub
Sub setDefaultToolTip(d as string)
// Set the default tool tip to use
Self.defaultToolTip = d
End Sub
Sub setImageMap(imageMap as string)
// Set image map used by the chart
//
//delete the existing ImageMapHandler
Self.currentHotSpot = -1
Self.isClickable = False
//create a new ImageMapHandler to represent the image map
If imageMap = "" Then
Self.hotSpotTester = Nil
Else
Self.hotSpotTester = New CDImageMapHandlerMBS(imageMap)
End If
End Sub
Sub setMinimumDrag(offset as integer)
// Set the minimum mouse drag before the dragging is considered as real. This is to avoid small
// mouse vibrations triggering a mouse drag.
Self.minDragAmount = offset
End Sub
Sub setMouseUsage(m as integer)
// Set the mouse usage mode
Self.mouseUsage = m
End Sub
Sub setMouseWheelZoomRatio(ratio as Double)
// Set the mouse wheel zoom ratio
Self.mouseWheelZoomRatio = ratio
End Sub
Sub setRectVisible(b as Boolean)
// Show/hide selection rectangle
// Create the edges of the rectangle if not already created
// Show/hide the edges
If TopLine <> Nil Then
TopLine.Visible = b
LeftLine.Visible = b
BottomLine.Visible = b
RightLine.Visible = b
End If
End Sub
Sub setScrollDirection(value as integer)
// Set the scroll direction
Self.scrollDirection = value
End Sub
Sub setSelectionBorderColor(c as color)
Self.selectBoxLineColor = c
If TopLine <> Nil Then
TopLine.LineColor = c
End If
If LeftLine <> Nil Then
LeftLine.LineColor = c
End If
If RightLine <> Nil Then
RightLine.LineColor = c
End If
If BottomLine <> Nil Then
BottomLine.LineColor = c
End If
End Sub
Sub setSelectionBorderWidth(width as integer)
// Set the border width of the selection box
Self.selectBoxLineWidth = width
End Sub
Sub setUpdateInterval(interval as integer)
// Set the minimum interval between ViewPortChanged events. This is to avoid the chart being
// updated too frequently. (Default is 20ms between chart updates.) Multiple update events
// arrived during the interval will be merged into one chart update and executed at the end
// of the interval.
Self.updateInterval = interval
End Sub
Sub setZoomDirection(value as integer)
// Set the zoom direction
Self.zoomDirection = value
End Sub
Sub setZoomInRatio(value as double)
// Set the zoom-in ratio for mouse click zoom-in
Self.zoomInRatio = value
End Sub
Sub setZoomOutRatio(value as Double)
// Set the zoom-out ratio
Self.zoomOutRatio = value
End Sub
Function startTimer(Interval as integer) As timer
Dim t As New timer
AddHandler t.Action, WeakAddressOf timerEvent
t.Period = Interval
t.Mode = timer.ModeMultiple
Return t
End Function
Sub timerEvent(timerId as timer)
// Chart hold timer.
If (delayedMouseEvent <> nil and (timerId = delayedMouseEventTimerId)) Then
// Is a delayed mouse move event
onDelayedMouseMove
Elseif (holdTimerActive And (timerId = holdTimerId)) then
holdTimerId.mode = timer.ModeOff
holdTimerActive = False
// If has pending chart update request, handles them now.
If (self.needUpdateChart Or self.needUpdateImageMap) Then
updateViewPort(self.needUpdateChart, self.needUpdateImageMap)
End If
End If
End Sub
Sub updateDisplay()
// Update the display
If delayUpdateChart = NO_DELAY Then
commitUpdateChart
Else
delayUpdateChart = NEED_UPDATE
delayedChart = currentChart
End If
End Sub
Sub updateViewPort(NeedUpdateChartParam as Boolean, needUpdateImageMapParam as Boolean)
// Trigger the ViewPortChanged event
// Already updating, no need to update again
If (Self.isInViewPortChanged) Then
Return
end if
// Merge the current update requests with any pending requests.
Self.needUpdateChart = Self.needUpdateChart Or needUpdateChartParam
self.needUpdateImageMap = needUpdateImageMapParam
// Hold timer has not expired, so do not update chart immediately. Keep the requests pending.
If (Self.holdTimerActive) Then
Return
end if
// The chart can be updated more than once during mouse move. For example, it can update due to
// drag to scroll, and also due to drawing track cursor. So we delay updating the display until
// all all updates has occured.
Dim hasDelayUpdate As Boolean = (Self.delayUpdateChart <> NO_DELAY)
If (Not hasDelayUpdate) Then
Self.delayUpdateChart = NEED_DELAY
End If
// Can trigger the ViewPortChanged event.
validateViewPort
Self.isInViewPortChanged = True
RaiseEvent viewPortChanged
Self.isInViewPortChanged = False
// Can update chart now
If (Not hasDelayUpdate) Then
commitUpdateChart
End If
// Clear any pending updates.
Self.needUpdateChart = False
self.needUpdateImageMap = False
// Set hold timer to prevent multiple chart updates within a short period.
If (Self.updateInterval > 0) Then
Self.holdTimerActive = True
Self.holdTimerId = startTimer(self.updateInterval)
End If
End Sub
Sub wheelEvent(MouseEvent as MouseEvent)
Dim hasReceivers As Boolean = True // receivers(SIGNAL(mouseWheel(QWheelEvent *))) > 0;
If (hasReceivers) Then
// Process delayed mouse move, if any
onDelayedMouseMove
// emit mouseWheel event
RaiseEvent mouseWheel(MouseEvent)
End If
// Process the mouse wheel only if the mouse is over the plot area
Dim hasMouseWheelZoom As Boolean = isMouseOnPlotArea And onMouseWheelZoom(getPlotAreaMouseX, getPlotAreaMouseY, MouseEvent.delta)
If (not (hasReceivers or hasMouseWheelZoom)) then
// ignore
End If
End Sub
Property BottomLine As Line
Property ChartPicture As Picture
Property HelpLabel As label
Property LeftLine As Line
Property RightLine As Line
Property TopLine As Line
Property autoHideMsg As string
Property currentChart As CDBaseChartMBS
// Current BaseChart object
Property currentHotSpot As Integer
// The hot spot under the mouse cursor.
Property currentMouseX As Integer
// Get the mouse x-pixel coordinate
Property currentMouseY As Integer
// Get the mouse y-pixel coordinate
Property defaultToolTip As string
// Default tool tip text
Property delayImageMapUpdate As Boolean
Property delayUpdateChart As Integer
Property delayedChart As CDBaseChartMBS
Property delayedMouseEvent As MouseEvent
Property delayedMouseEventTimerId As Timer
Property holdTimerActive As Boolean
// Delay chart update to limit update frequency
Property holdTimerId As timer
Property hotSpotTester As CDImageMapHandlerMBS
// ImageMapHander representing the image map
Property isClickable As Boolean
// Mouse is over a clickable hot spot.
Property isDragScrolling As Boolean
// Is current dragging scrolling the chart.
Property isInMouseMove As Boolean
// Is in mouse moeve event handler
Property isInMouseMovePlotArea As Boolean
// flag to indicate if is in a mouse move plot area event.
Property isInViewPortChanged As Boolean
Property isMouseTracking As Boolean
// Is tracking mouse leave event
Property isOnPlotArea As Boolean
// Mouse is over the plot area.
Property isPlotAreaMouseDown As Boolean
// Mouse left button is down in the plot area.
Property lastMouseMove As Integer
Property minDragAmount As Integer
// Minimum drag amount
Property mouseUsage As Integer
// Mouse usage mode
Property mouseWheelZoomRatio As double
// Mouse wheel zoom ratio
Property needUpdateChart As Boolean
// Has pending chart update request
Property needUpdateImageMap As Boolean
// Has pending image map udpate request
Property plotAreaMouseDownXPos As Integer
Property plotAreaMouseDownYPos As Integer
Property scrollDirection As Integer
// Scroll direction
Property selectBoxLineColor As color
// Selectiom box border color
Property selectBoxLineWidth As Integer
// Selectiom box border width
Property updateInterval As Integer
// Minimum interval between chart updates
Property zoomDirection As Integer
// Zoom direction
Property zoomInRatio As double
// Click zoom in ratio
Property zoomOutRatio As double
// Click zoom out ratio
End Class
Module UtilModule
Function DoubleArray(values() as double, Count as Integer, Offset as Integer = 0) As Double()
Dim u As Integer = values.Ubound
Dim c As Integer = u + 1
If Offset = 0 And c = count Then
// full array
Return values
End If
Dim r() As Double
u = Offset + count - 1
For i As Integer = Offset To u
r.Append values(i)
Next
Return r
End Function
End Module
Class ViewPortControl Inherits CDViewPortControlBaseMBS
Sub Close()
If Self.output <> Nil Then
Self.output.ViewPortControl = Nil
Self.Output = Nil
End If
Self.viewer = Nil
End Sub
Sub Constructor()
// Calling the overridden superclass constructor.
Super.Constructor
Self.Viewer = Nil
Self.mouseDownX = 0
Self.mouseDownY = 0
End Sub
Function getChart() As CDBaseChartMBS
// Get back the same BaseChart pointer provided by the previous setChart call.
Return Self.Chart
End Function
Function getViewer() As ChartViewerCanvas
Return viewer
End Function
Function isDrag(MouseEvent as MouseEvent) As Boolean
// Determines if the mouse is dragging.
If viewer = Nil Then
Return False
End If
Dim minimumDrag As Integer = viewer.getMinimumDrag
Dim moveX As Integer = Abs(Self.mouseDownX - MouseEvent.x)
Dim moveY As Integer = Abs(Self.mouseDownY - MouseEvent.y)
Return (moveX >= minimumDrag) Or (moveY >= minimumDrag)
End Function
Sub mouseMoveEvent(MouseEvent as MouseEvent)
// MouseMove event handler
//
// Get the QChartViewer zoom/scroll state to determine which type of mouse action is allowed
syncState
// Handle the mouse move event
handleMouseMove(toImageX(MouseEvent.x), toImageY(MouseEvent.y), isDrag(MouseEvent))
// Update the chart viewer if the viewport has changed
updateChartViewerIfNecessary
// Update the mouse cursor
updateCursor(Self.Cursor)
// Update the display
If (needUpdateDisplay) Then
paintDisplay
End If
End Sub
Sub mousePressEvent(MouseEvent as MouseEvent)
// Mouse button down event.
If (MouseEvent.button <> MouseEvent.LeftButton) then
Return
End If
// Remember current mouse position
Self.mouseDownX = MouseEvent.x
self.mouseDownY = MouseEvent.y
// Get the QChartViewer zoom/scroll state to determine which type of mouse action is allowed
syncState
// Handle the mouse down event
handleMouseDown(toImageX(MouseEvent.x), toImageY(MouseEvent.y))
// Update the chart viewer if the viewport has changed
updateChartViewerIfNecessary
End Sub
Sub mouseReleaseEvent(MouseEvent as MouseEvent)
// Mouse button up event.
If (MouseEvent.button <> MouseEvent.LeftButton) Then
Return
End If
// Get the QChartViewer zoom/scroll state to determine which type of mouse action is allowed
syncState
// Handle the mouse down event
handleMouseUp(toImageX(MouseEvent.x), toImageY(MouseEvent.y))
// Update the chart viewer if the viewport has changed
updateChartViewerIfNecessary
End Sub
Sub onViewPortChanged()
// Handle the ViewPortChanged event from the associated ChartViewer
Self.updateDisplay
End Sub
Sub paintDisplay()
// Paint the display
Dim c As CDBaseChartMBS = Self.Chart
If c <> Nil Then
Output.ChartPicture = c.makeChartPicture
Else
System.DebugLog "No chart in "+CurrentMethodName
Output.ChartPicture = Nil
End If
Output.invalidate
End Sub
Sub setChart(c as CDXYChartMBS)
// Set the chart to be displayed in the control
if c <> nil then
Self.Chart = c
Self.updateDisplay
Else
Break
End If
End Sub
Sub setViewer(v as ChartViewerCanvas)
// Set the CChartViewer to be associated with this control
Self.viewer = v
self.ViewPortManager = v.ViewPortManager
updateDisplay
End Sub
Sub syncState()
// Synchronize the CViewPortControl with CChartViewer
If viewer <> Nil Then
self.setZoomScrollDirection viewer.zoomDirection, viewer.scrollDirection
End If
End Sub
Function toImageX(x as integer) As double
// In this version, no conversion is done. It is assumed the control does not stretch or shrink
// the image and does not provide any additional margin to offset the image.
Return x
End Function
Function toImageY(y as integer) As double
// In this version, no conversion is done. It is assumed the control does not stretch or shrink
// the image and does not provide any additional margin to offset the image.
Return y
End Function
Sub updateChartViewerIfNecessary()
// Update ChartViewer if viewport has changed
If viewer = Nil Then
Break
Return
End If
If (Self.needUpdateChart Or Self.needUpdateImageMap) Then
viewer.updateViewPort(self.needUpdateChart, self.needUpdateImageMap)
End If
End Sub
Sub updateCursor(position as integer)
// Update the mouse cursor
Select Case position
Case CDBaseChartMBS.kLeft, CDBaseChartMBS.kRight
output.MouseCursor = System.Cursors.ArrowEastWest
Case CDBaseChartMBS.kTop, CDBaseChartMBS.kBottom
output.MouseCursor = System.Cursors.ArrowNorthSouth
Case CDBaseChartMBS.kTopLeft, CDBaseChartMBS.kBottomRight
output.MouseCursor = System.Cursors.ArrowNorthwestSoutheast
Case CDBaseChartMBS.kTopRight, CDBaseChartMBS.kBottomLeft
output.MouseCursor = System.Cursors.ArrowNortheastSouthwest
else
output.MouseCursor = Nil
End Select
End Sub
Sub updateDisplay()
// Update the display
paintViewPort
paintDisplay
End Sub
Sub wheelEvent(MouseEvent as MouseEvent)
// MouseWheel handler
// Process the mouse wheel only if the mouse is over the plot area
If ((Nil = Viewer) Or (Not isOnPlotArea(MouseEvent.x, MouseEvent.y))) Then
// ignore
Else
// Ask the CChartViewer to zoom around the center of the chart
Dim x As Integer = Viewer.getPlotAreaLeft + Viewer.getPlotAreaWidth / 2
Dim y As Integer = Viewer.getPlotAreaTop + Viewer.getPlotAreaHeight / 2
If (Not viewer.onMouseWheelZoom(x, y, MouseEvent.delta)) then
// ignore
End If
End If
End Sub
Property Output As ViewPortControlCanvas
Property Viewer As ChartViewerCanvas
Property mouseDownX As Integer
Property mouseDownY As Integer
End Class
FileTypes1
Filetype image/png
End FileTypes1
Class ViewPortManagerCanvas Inherits Canvas
Sub Constructor()
self.ViewPortManager = New CDViewPortManagerMBS
// Calling the overridden superclass constructor.
Super.Constructor
End Sub
Function canZoomIn(zoomDirection as integer) As boolean
return ViewPortManager.canZoomIn(zoomDirection)
End Function
Function canZoomOut(zoomDirection as integer) As boolean
return ViewPortManager.canZoomOut(zoomDirection)
End Function
Sub clearAllRanges()
ViewPortManager.clearAllRanges
End Sub
Sub commitPendingSyncAxis(baseChart as CDBaseChartMBS)
ViewPortManager.commitPendingSyncAxis(baseChart)
End Sub
Function dragTo(scrollDirection as Integer, x as Integer, y as Integer) As boolean
Return ViewPortManager.dragTo(scrollDirection, x, y)
End Function
Function getPlotAreaHeight() As integer
Return ViewPortManager.getPlotAreaHeight
End Function
Function getPlotAreaLeft() As integer
Return ViewPortManager.getPlotAreaLeft
End Function
Function getPlotAreaTop() As integer
Return ViewPortManager.getPlotAreaTop
End Function
Function getPlotAreaWidth() As integer
Return ViewPortManager.getPlotAreaWidth
End Function
Function getValueAtViewPort(id as string, ratio as double, isLogScale as boolean = false) As double
Return ViewPortManager.getValueAtViewPort(id, ratio, isLogScale)
End Function
Function getViewPortAtValue(id as string, ratio as double, isLogScale as boolean = false) As double
Return ViewPortManager.getViewPortAtValue(id, ratio, isLogScale)
End Function
Function getViewPortHeight() As Double
Return ViewPortManager.getViewPortHeight
End Function
Function getViewPortLeft() As Double
return ViewPortManager.getViewPortLeft
End Function
Function getViewPortTop() As Double
return ViewPortManager.getViewPortTop
End Function
Function getViewPortWidth() As Double
return ViewPortManager.getViewPortWidth
End Function
Function inExtendedPlotArea(x as integer, y as integer) As boolean
Return ViewPortManager.inExtendedPlotArea(x,y)
End Function
Function inPlotArea(x as Integer, y as Integer) As boolean
Return ViewPortManager.inPlotArea(x,y)
End Function
Sub setChartMetrics(metrics as string)
ViewPortManager.setChartMetrics metrics
End Sub
Sub setFullRange(ID as string, minValue as Double, maxValue as Double)
ViewPortManager.setFullRange(ID, minValue, maxValue)
End Sub
Sub setPlotAreaMouseMargin(leftMargin as Integer, rightMargin as Integer, topMargin as Integer, bottomMargin as Integer)
ViewPortManager.setPlotAreaMouseMargin(leftMargin, rightMargin, topMargin, bottomMargin)
End Sub
Sub setViewPortHeight(value as double)
'System.DebugLog CurrentMethodName + " " + Str(value)
ViewPortManager.setViewPortHeight(value)
End Sub
Sub setViewPortLeft(value as double)
System.DebugLog CurrentMethodName + " " + Str(value)
ViewPortManager.setViewPortLeft(value)
End Sub
Sub setViewPortTop(value as double)
'System.DebugLog CurrentMethodName + " " + Str(value)
ViewPortManager.setViewPortTop(value)
End Sub
Sub setViewPortWidth(value as double)
'System.DebugLog CurrentMethodName + " " + Str(value)
ViewPortManager.setViewPortWidth(value)
End Sub
Sub setZoomInHeightLimit(value as double)
ViewPortManager.setZoomInHeightLimit(value)
End Sub
Sub setZoomInWidthLimit(value as double)
ViewPortManager.setZoomInWidthLimit(value)
End Sub
Sub setZoomOutHeightLimit(value as double)
System.DebugLog CurrentMethodName + " " + Str(value)
ViewPortManager.setZoomOutHeightLimit(value)
End Sub
Sub setZoomOutWidthLimit(value as double)
System.DebugLog CurrentMethodName + " " + Str(value)
ViewPortManager.setZoomOutWidthLimit(value)
End Sub
Sub startDrag()
ViewPortManager.startDrag
End Sub
Function updateFullRangeH(id as string, minValue as double, maxValue as double, updateType as Integer) As boolean
'System.DebugLog CurrentMethodName+" minValue: "+Str(minValue)+" maxValue: "+Str(maxValue)
Return ViewPortManager.updateFullRangeH(id, minValue, maxValue, updateType)
End Function
Function updateFullRangeV(id as string, minValue as double, maxValue as double, updateType as Integer) As boolean
Return ViewPortManager.updateFullRangeV(id, minValue, maxValue, updateType)
End Function
Sub validateViewPort()
ViewPortManager.validateViewPort
End Sub
Function zoomAround(x as Integer, y as Integer, xZoomRatio as Double, yZoomRatio as Double) As boolean
Return ViewPortManager.zoomAround(x, y, xZoomRatio, yZoomRatio)
End Function
Function zoomAt(zoomDirection as Integer, x as Integer, y as Integer, zoomRatio as Double) As boolean
Return ViewPortManager.zoomAt(zoomDirection, x, y, zoomRatio)
End Function
Function zoomTo(zoomDirection as Integer, x1 as Integer, y1 as Integer, x2 as Integer, y2 as Integer) As boolean
return ViewPortManager.zoomTo(zoomDirection, x1, y1, x2, y2)
End Function
Note "About this class"
In C++ you can have a class inherit from multiple classes
In Xojo you can't.
So we have this intermediate class to forward all calls to
Once this code is fully debugged and running, someone may remove this intermediate class and change all calls to the methods with ViewPortManager. prefix.
Property ViewPortManager As CDViewPortManagerMBS
End Class
Class MouseEvent
Const LeftButton = 1
Const NoButton = 0
Const RightButton = 2
Sub Constructor(MouseX as Integer, MouseY as integer)
Self.x = mouseX
Self.y = MouseY
End Sub
Sub SetButton()
If IsContextualClick Then
button = RightButton
Else
button = LeftButton
End If
End Sub
Property button As Integer
Property delta As Integer
Property x As Integer
Property y As Integer
End Class
Class ViewPortControlCanvas Inherits Canvas
EventHandler Function MouseDown(X As Integer, Y As Integer) As Boolean
'System.DebugLog CurrentMethodName+" "+Str(x)+"/"+Str(y)
Dim m As New MouseEvent(x,y)
m.SetButton
ViewPortControl.mousePressEvent(m)
Return True
End EventHandler
EventHandler Sub MouseDrag(X As Integer, Y As Integer)
'System.DebugLog CurrentMethodName+" "+Str(x)+"/"+Str(y)
Dim m As New MouseEvent(x,y)
ViewPortControl.mouseMoveEvent m
End EventHandler
EventHandler Sub MouseMove(X As Integer, Y As Integer)
'System.DebugLog CurrentMethodName+" "+Str(x)+"/"+Str(y)
Dim m As New MouseEvent(x,y)
ViewPortControl.mouseMoveEvent m
End EventHandler
EventHandler Sub MouseUp(X As Integer, Y As Integer)
'System.DebugLog CurrentMethodName+" "+Str(x)+"/"+Str(y)
Dim m As New MouseEvent(x,y)
m.SetButton
ViewPortControl.mouseReleaseEvent(m)
End EventHandler
EventHandler Function MouseWheel(X As Integer, Y As Integer, deltaX as Integer, deltaY as Integer) As Boolean
Dim m As New MouseEvent(x,y)
m.delta = deltay
ViewPortControl.wheelEvent(m)
#Pragma Unused deltaX
End EventHandler
EventHandler Sub Paint(g As Graphics, areas() As REALbasic.Rect)
'System.DebugLog CurrentMethodName
If ChartPicture <> Nil Then
g.DrawPicture ChartPicture, 0, 0, g.Width, g.height, 0, 0, ChartPicture.Width, ChartPicture.Height
Else
System.DebugLog "No chart picture in "+CurrentMethodName
End If
#Pragma Unused areas
End EventHandler
Property ChartPicture As Picture
Property ViewPortControl As ViewPortControl
End Class
End Project
The items on this page are in the following plugins: MBS ChartDirector Plugin.