ChartDirector 7.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