ChartDirector 6.0 (ASP/COM/VB Edition)

Zooming and Scrolling Demonstration (Windows)




This sample program demonstrates a zoomable and scrollable chart for time based data series. The followings are the key elements of this sample program:

The main source code listing of this sample program is included at the end of this section. The code consists of the following main parts:

Source Code Listing

[Windows Version (in Visual Basic)] vbdemo\frmZoomScrollDemo.frm
Option Explicit

Private cd As New ChartDirector.API

' Data arrays for the scrollable / zoomable chart.
Private timeStamps()
Private dataSeriesA()
Private dataSeriesB()
Private dataSeriesC()
        
' The earliest date and the duration in seconds for horizontal scrolling
Private minDate As Date
Private dateRange As Double
        
' The vertical range of the chart for vertical scrolling
Private maxValue As Double
Private minValue As Double
        
' The current visible duration of the view port in seconds
Private currentDuration As Double
' In this demo, the maximum zoom-in is set to 10 days
Private minDuration As Double

' A flag to track if the user has entered into the duration combo box
Private durationHasChanged As Boolean

'
' Visual Basic 6 does not have a built in binary search function, so we need to write
' one ourselves.
'
' This binary search function will return the lower index if there is no exact match.
' For example, if the target is between elements 3 and 4, it will return 3. If the
' target is beyond the first element of the array, it will return LBound(arr) - 1.
'
Private Function binarySearch(arr, target)
    Dim lowerIndex As Long
    Dim upperIndex As Long
    Dim midIndex As Long
    Dim isAscending As Long
    
    lowerIndex = LBound(arr)
    upperIndex = UBound(arr)
    isAscending = Sgn(arr(upperIndex) - arr(lowerIndex))
    
    Do While lowerIndex <= upperIndex
        midIndex = (lowerIndex + upperIndex) / 2
        Select Case Sgn(arr(midIndex) - target) * isAscending
            Case 0
                binarySearch = midIndex
                Exit Function
            Case -1
                lowerIndex = midIndex + 1
            Case 1
                upperIndex = midIndex - 1
        End Select
    Loop
    
    binarySearch = IIf(lowerIndex > upperIndex, upperIndex, lowerIndex)
End Function

'
' Initialize the Form
'
Private Sub Form_Load()

    ' Load the data
    loadData
    
    ' In this demo, we deduce the horizontal scroll range from the actual data.
    minDate = timeStamps(0)
    dateRange = DateDiff("s", minDate, timeStamps(UBound(timeStamps)))
    
    ' In this demo, the maximum zoom-in is set to 10 days (1 day = 86400 seconds)
    minDuration = 10 * 86400
    
    ' Set ChartViewer to reflect the visible and minimum duration
    ChartViewer1.ZoomInWidthLimit = minDuration / dateRange
    ChartViewer1.ViewportWidth = currentDuration / dateRange
    ChartViewer1.ViewportLeft = 1 - ChartViewer1.ViewportWidth
    
    ' Initially choose the pointer mode (drag to scroll mode)
    PointerPB_Click
    XZoomPB_Click

End Sub

'
' Load the data
'
Private Sub loadData()

    ' In this demo, we allow scrolling the chart for the last 5 years
    Dim lastDate As Date
    lastDate = Date
    Dim firstDate As Date
    firstDate = DateAdd("yyyy", -5, lastDate)

    ' The initial view port is to show 1 year of data.
    Dim viewPortStartDate As Date
    viewPortStartDate = DateAdd("yyyy", -1, lastDate)
    currentDuration = DateDiff("s", viewPortStartDate, lastDate)
        
    '
    ' Get the data and stores them in a memory buffer for fast scrolling / zooming. In
    ' this demo, we just use a random number generator. In practice, you may get the data
    ' from a database or XML or by other means. (See the ChartDirector documentation on
    ' "Using Data Sources with ChartDirector" if you need some sample code on how to read
    ' data from database to array variables.)
    '

    ' Set up random number generator
    Dim noOfDays As Integer
    noOfDays = DateDiff("d", firstDate, lastDate) + 1
    Dim r As rantable
    Set r = cd.rantable(127, 4, noOfDays)
    Call r.setDateCol(0, firstDate, 86400)
    Call r.setCol(1, 150, -10, 10)
    Call r.setCol(2, 200, -10, 10)
    Call r.setCol(3, 250, -10, 10)
        
    ' Read random data into the data arrays
    timeStamps = cd.NTime(r.getCol(0))
    dataSeriesA = r.getCol(1)
    dataSeriesB = r.getCol(2)
    dataSeriesC = r.getCol(3)
End Sub

'
' User clicks on the Pointer pushbutton
'
Private Sub PointerPB_Click()
    ChartViewer1.MouseUsage = cvScrollOnDrag
End Sub

'
' User clicks on the Zoom In pushbutton
'
Private Sub ZoomInPB_Click()
    ChartViewer1.MouseUsage = cvZoomIn
End Sub

'
' User clicks on the Zoom Out pushbutton
'
Private Sub ZoomOutPB_Click()
    ChartViewer1.MouseUsage = cvZoomOut
End Sub

'
' User clicks on the X-Zoom pushbutton
'
Private Sub XZoomPB_Click()
    ChartViewer1.ScrollDirection = cvHorizontal
    ChartViewer1.ZoomDirection = cvHorizontal
            
    ' Viewport is always unzoomed as y-axis is auto-scaled
    ChartViewer1.ViewportTop = 0
    ChartViewer1.ViewportHeight = 1
        
    ' Update chart to auto-scale axis
    Call ChartViewer1.UpdateViewPort(True, True)
End Sub

'
' User clicks on the XY-Zoom pushbutton
'
Private Sub XYZoomPB_Click()
    ChartViewer1.ScrollDirection = cvHorizontalVertical
    ChartViewer1.ZoomDirection = cvHorizontalVertical
End Sub

'
' User selects a start date from the date/time picker control
'
Private Sub StartDate_Change()
    ' Compute the new view port position based on the selected date
    ChartViewer1.ViewportLeft = DateDiff("s", minDate, StartDate.value) / dateRange
    Call ChartViewer1.UpdateViewPort(True, True)
End Sub

'
' User selects a duration from the Duration combo box
'
Private Sub Duration_Click()
    durationHasChanged = False
    
    ' Validate user input
    Dim enteredDuration As Double
    On Error GoTo FormatError
    enteredDuration = CDbl(Duration.Text) * 86400
    On Error GoTo 0
    If enteredDuration <= minDuration Then
        enteredDuration = minDuration
    End If
        
    ' Check if duration has really changed - sometimes the combo box may issue
    ' redundant value changed events when value has not actually changed.
    Dim newViewPortWidth As Double
     newViewPortWidth = enteredDuration / dateRange
    If (Abs(ChartViewer1.ViewportWidth - newViewPortWidth) > _
        0.00001 * ChartViewer1.ViewportWidth) Then
        ' Set the view port based on the duration
        ChartViewer1.ViewportWidth = newViewPortWidth
        Call ChartViewer1.UpdateViewPort(True, True)
    End If
    
    Exit Sub

FormatError:

    Duration.Text = Int(0.5 + ChartViewer1.ViewportWidth * dateRange / 86400)
    durationHasChanged = False

End Sub

'
' The Duration combo box lost focus (User may have entered a new duration.)
'
Private Sub Duration_LostFocus()
    If durationHasChanged Then
        Duration_Click
    End If
End Sub

'
' User presses "Enter" key. (User may have entered a new duration.)
'
Private Sub Duration_KeyPress(KeyAscii As Integer)
    If KeyAscii = 13 Then
        Duration_LostFocus
    Else
        durationHasChanged = True
    End If
End Sub

'
' User clicks on the the horizontal scroll bar
'
Private Sub HScrollBar1_Change()
    Call HScrollBar1_Scroll
End Sub

Private Sub HScrollBar1_Scroll()
    ' Set the view port based on the scroll bar
    ChartViewer1.ViewportLeft = CDbl(HScrollBar1.value - HScrollBar1.Min) / _
        (HScrollBar1.Max - HScrollBar1.Min) * (1 - ChartViewer1.ViewportWidth)
    Call ChartViewer1.UpdateViewPort(True, False)
End Sub

'
' User clicks on the the vertical scroll bar
'
Private Sub VScrollBar1_Change()
    Call VScrollBar1_Scroll
End Sub

Private Sub VScrollBar1_Scroll()
    ' Set the view port based on the scroll bar
    ChartViewer1.ViewportTop = CDbl(VScrollBar1.value - VScrollBar1.Min) / _
        (VScrollBar1.Max - VScrollBar1.Min) * (1 - ChartViewer1.ViewportHeight)
    Call ChartViewer1.UpdateViewPort(True, False)
End Sub

'
' Mouse moves over ChartViewer
'
Private Sub ChartViewer1_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
    If Button = 0 Then
        ' Mouse is over chart with mouse button released (not dragging)
        ' Update image map if necessary
        Call updateImageMap(ChartViewer1)
    End If
End Sub

'
' User clicks on a hot spot on the chart
'
Private Sub ChartViewer1_ClickHotSpot(hotSpot As Collection, Button As Integer, Shift As Integer, _
     X As Single, Y As Single)
    ' We show the pop up dialog only when the mouse action is not zoom in or zoom out
    If ChartViewer1.MouseUsage <> cvZoomIn And ChartViewer1.MouseUsage <> cvZoomOut Then
        'In this demo, just list out the information provided by ChartDirector about hot spot
        ParamViewer.Display hotSpot
    End If
End Sub

'
' ChartViewer ViewPortChanged event
'
Private Sub ChartViewer1_ViewPortChanged(needUpdateChart As Boolean, needUpdateImageMap As Boolean)

    ' Set the date/time picker and duration box to reflect the current view port
    StartDate.value = DateAdd("s", ChartViewer1.ViewportLeft * dateRange, minDate)
    Duration.Text = Int(0.5 + dateRange * ChartViewer1.ViewportWidth / 86400)
        
    ' Set the horizontal scroll bar to reflect the current view port
    HScrollBar1.Enabled = ChartViewer1.ViewportWidth < 1
    If HScrollBar1.Enabled Then
        HScrollBar1.LargeChange = Int(ChartViewer1.ViewportWidth * _
            (HScrollBar1.Max - HScrollBar1.Min) + 0.99999999999)
        HScrollBar1.SmallChange = Int(HScrollBar1.LargeChange * 0.1 + 0.99999999999)
        HScrollBar1.value = CInt(ChartViewer1.ViewportLeft / (1 - ChartViewer1.ViewportWidth) * _
            (HScrollBar1.Max - HScrollBar1.Min)) + HScrollBar1.Min
    End If
    
    ' Set the horizontal scroll bar to reflect the current view port
    VScrollBar1.Enabled = ChartViewer1.ViewportHeight < 1
    If VScrollBar1.Enabled Then
        VScrollBar1.LargeChange = Int(ChartViewer1.ViewportHeight * _
            (VScrollBar1.Max - VScrollBar1.Min) + 0.99999999999)
        VScrollBar1.SmallChange = Int(VScrollBar1.LargeChange * 0.1 + 0.99999999999)
        VScrollBar1.value = CInt(ChartViewer1.ViewportTop / (1 - ChartViewer1.ViewportHeight) * _
            (VScrollBar1.Max - VScrollBar1.Min)) + VScrollBar1.Min
    End If
    
    
    ' Update chart and image map if necessary
    If needUpdateChart Then
        Call drawChart(ChartViewer1)
    End If
    If needUpdateImageMap Then
        Call updateImageMap(ChartViewer1)
    End If
    
End Sub

   
'
' Draw the chart
'
Sub drawChart(viewer As ChartViewer)
    '
    ' In this demo, we copy the visible part of the data to a separate buffer for chart
    ' plotting.
    '
    ' Note that if you only have a small amount of data (a few hundred data points), it
    ' may be easier to just plot all data in any case (so the following copying code is
    ' not needed), and let ChartDirector "clip" the chart to the plot area.
    '

    ' Using ViewPortLeft and ViewPortWidth, get the start and end dates of the view port.
    Dim viewPortStartDate As Date
    Dim viewPortEndDate As Date
    viewPortStartDate = DateAdd("s", viewer.ViewportLeft * dateRange, minDate)
    viewPortEndDate = DateAdd("s", viewer.ViewportWidth * dateRange, viewPortStartDate)

    ' Get the starting index of the array using the start date
    Dim startIndex As Long
    startIndex = binarySearch(timeStamps, viewPortStartDate)
    If startIndex < 0 Then
        startIndex = 0
    End If
           
    ' Get the ending index of the array using the end date
    Dim endIndex As Long
    endIndex = binarySearch(timeStamps, viewPortEndDate)
    If timeStamps(endIndex) <> viewPortEndDate And endIndex < UBound(timeStamps) Then
        endIndex = endIndex + 1
    End If
    
    ' Get the length
    Dim noOfPoints As Long
    noOfPoints = endIndex - startIndex + 1

    ' Now, we can just copy the visible data we need into the view port data series
    Dim viewPortTimeStamps()
    Dim viewPortDataSeriesA()
    Dim viewPortDataSeriesB()
    Dim viewPortDataSeriesC()
    viewPortTimeStamps = cd.ArrayMath(timeStamps).Trim(startIndex, noOfPoints).result()
    viewPortDataSeriesA = cd.ArrayMath(dataSeriesA).Trim(startIndex, noOfPoints).result()
    viewPortDataSeriesB = cd.ArrayMath(dataSeriesB).Trim(startIndex, noOfPoints).result()
    viewPortDataSeriesC = cd.ArrayMath(dataSeriesC).Trim(startIndex, noOfPoints).result()

    If UBound(viewPortTimeStamps) + 1 >= 520 Then
        '
        ' Zoomable chart with high zooming ratios often need to plot many thousands of points when
        ' fully zoomed out. However, it is usually not needed to plot more data points than the
        ' pixel resolution of the chart. Plotting too many points may cause the points and the lines
        ' to overlap on the same pixel. So rather than increasing resolution, this reduces the
        ' clarity of the chart. It is better to aggregate the data first if there are too many
        ' points.
        '
        ' In our current example, the chart plot area only has 520 pixels in width and is using a 2
        ' pixel line width. So if there are more than 520 data points, we aggregate the data using
        ' the ChartDirector aggregation utility method.
        '
        ' If in your real application, you do not have too many data points, you may remove the
        ' following code altogether.
        '

        ' Set up an aggregator to aggregate the data based on regular sized slots
        Dim m As ArrayMath
        Set m = cd.ArrayMath(viewPortTimeStamps)
        Call m.selectRegularSpacing((UBound(viewPortTimeStamps) + 1) / 260)

        ' For the timestamps, take the first timestamp on each slot
        viewPortTimeStamps = m.aggregate(viewPortTimeStamps, cd.AggregateFirst)

        ' For the data values, take the averages
        viewPortDataSeriesA = m.aggregate(viewPortDataSeriesA, cd.AggregateAvg)
        viewPortDataSeriesB = m.aggregate(viewPortDataSeriesB, cd.AggregateAvg)
        viewPortDataSeriesC = m.aggregate(viewPortDataSeriesC, cd.AggregateAvg)
    End If

    '
    ' Now we have obtained the data, we can plot the chart.
    '

    '================================================================================
    ' Step 1 - Configure overall chart appearance.
    '================================================================================

    ' Create an XYChart object 600 x 300 pixels in size, with pale blue (0xf0f0ff) background, black
    ' (000000) rounded border, 1 pixel raised effect.
    Dim c As XYChart
    Set c = cd.XYChart(600, 300, &HF0F0FF, &H0)
    Call c.setRoundedFrame

    ' Set the plotarea at (52, 60) and of size 520 x 192 pixels. Use white (ffffff) background.
    ' Enable both horizontal and vertical grids by setting their colors to grey (cccccc). Set
    ' clipping mode to clip the data lines to the plot area.
    Call c.setPlotArea(55, 60, 520, 192, &HFFFFFF, -1, -1, &HCCCCCC, &HCCCCCC)
    Call c.setClipping

    ' Add a top title to the chart using 15 pts Times New Roman Bold Italic font, with a light blue
    ' (ccccff) background, black (000000) border, and a glass like raised effect.
    Call c.addTitle("Zooming and Scrolling Demonstration", "timesbi.ttf", 15).setBackground( _
        &HCCCCFF, &H0, cd.glassEffect())

    ' Add a bottom title to the chart to show the date range of the axis, with a light blue (ccccff)
    ' background.
    Call c.addTitle2(cd.Bottom, "From <*font=arialbi.ttf*>" & c.formatValue(viewPortStartDate, _
        "{value|mmm dd, yyyy}") & "<*/font*> to <*font=arialbi.ttf*>" & c.formatValue( _
        viewPortEndDate, "{value|mmm dd, yyyy}") & "<*/font*> (Duration <*font=arialbi.ttf*>" & _
        Int(0.5 + (viewPortEndDate - viewPortStartDate)) & "<*/font*> days)", "ariali.ttf", 10 _
        ).setBackground(&HCCCCFF)

    ' Add a legend box at the top of the plot area with 9pts Arial Bold font with flow layout.
    Call c.addLegend(50, 33, False, "arialbd.ttf", 9).setBackground(cd.Transparent, cd.Transparent)

    ' Set axes width to 2 pixels
    Call c.xAxis().setWidth(2)
    Call c.yAxis().setWidth(2)

    ' Add a title to the y-axis
    Call c.yAxis().setTitle("Price (USD)", "arialbd.ttf", 10)

    '================================================================================
    ' Step 2 - Add data to chart
    '================================================================================

    '
    ' In this example, we represent the data by lines. You may modify the code below if you want 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 lineLayer
    Set layer = c.addLineLayer2()
    Call layer.setLineWidth(2)

    ' Now we add the 3 data series to a line layer, using the color red (ff0000), green (00cc00) and
    ' blue (0000ff)
    Call layer.setXData(viewPortTimeStamps)
    Call layer.addDataSet(viewPortDataSeriesA, &HFF0000, "Product Alpha")
    Call layer.addDataSet(viewPortDataSeriesB, &HCC00, "Product Beta")
    Call layer.addDataSet(viewPortDataSeriesC, &HFF, "Product Gamma")

    '================================================================================
    ' Step 3 - Set up x-axis scale
    '================================================================================

    ' Set x-axis date scale to the view port date range. ChartDirector auto-scaling will
    ' automatically determine the ticks on the axis.
    Call c.xAxis().setDateScale(viewPortStartDate, viewPortEndDate)

    '
    ' In the current demo, the x-axis range can be from a few years to a few days. We can let
    ' ChartDirector auto-determine the date/time format. However, for more beautiful formatting, we
    ' set up several label formats to be applied at different conditions.
    '

    ' If all ticks are yearly aligned, then we use "yyyy" as the label format.
    Call c.xAxis().setFormatCondition("align", 31104000)
    Call c.xAxis().setLabelFormat("{value|yyyy}")

    ' If all ticks are monthly aligned, then we use "mmm yyyy" in bold font as the first label of a
    ' year, and "mmm" for other labels.
    Call c.xAxis().setFormatCondition("align", 2592000)
    Call c.xAxis().setMultiFormat(cd.StartOfYearFilter(), "<*font=bold*>{value|mmm yyyy}", _
        cd.AllPassFilter(), "{value|mmm}")

    ' If all ticks are daily algined, then we use "mmm dd<*br*>yyyy" in bold font as the first label
    ' of a year, and "mmm dd" in bold font as the first label of a month, and "dd" for other labels.
    Call c.xAxis().setFormatCondition("align", 86400)
    Call c.xAxis().setMultiFormat(cd.StartOfYearFilter(), _
        "<*block,halign=left*><*font=bold*>{value|mmm dd<*br*>yyyy}", cd.StartOfMonthFilter(), _
        "<*font=bold*>{value|mmm dd}")
    Call c.xAxis().setMultiFormat2(cd.AllPassFilter(), "{value|dd}")

    ' For all other cases (sub-daily ticks), use "hh:nn<*br*>mmm dd" for the first label of a day,
    ' and "hh:nn" for other labels.
    Call c.xAxis().setFormatCondition("else")
    Call c.xAxis().setMultiFormat(cd.StartOfDayFilter(), "<*font=bold*>{value|hh:nn<*br*>mmm dd}", _
        cd.AllPassFilter(), "{value|hh:nn}")

    '================================================================================
    ' Step 4 - Set up y-axis scale
    '================================================================================

    If viewer.ZoomDirection = cvHorizontal Or minValue = maxValue Then
        ' y-axis is auto-scaled - save the chosen y-axis scaled to support xy-zoom mode
        Call c.layout
        minValue = c.yAxis().getMinValue()
        maxValue = c.yAxis().getMaxValue()
    Else
        ' xy-zoom mode - compute the actual axis scale in the view port
        Dim axisLowerLimit As Double
        Dim axisUpperLimit As Double
        axisLowerLimit = maxValue - (maxValue - minValue) * _
            (viewer.ViewportTop + viewer.ViewportHeight)
        axisUpperLimit = maxValue - (maxValue - minValue) * viewer.ViewportTop
        ' *** use the following formula if you are using a log scale axis ***
        ' axisLowerLimit = maxValue * ((minValue / maxValue) ^ _
        '     (viewer.ViewPortTop + viewer.ViewPortHeight))
        ' axisUpperLimit = maxValue * ((minValue / maxValue) ^ viewer.ViewPortTop)

        ' use the zoomed-in scale
        Call c.yAxis().setLinearScale(axisLowerLimit, axisUpperLimit)
        Call c.yAxis().setRounding(False, False)
    End If
    
    '================================================================================
    ' Step 5 - Display the chart
    '================================================================================

    Set viewer.Chart = c
    
End Sub

'
' Apply image map used on the chart if not already applied
'
Private Sub updateImageMap(viewer As ChartViewer)
    If viewer.ImageMap = "" Then
        viewer.ImageMap = viewer.Chart.getHTMLImageMap("clickable", "", _
            "title='[{dataSetName}] {x|mmm dd, yyyy}: USD {value|2}'")
    End If
End Sub