Selectable 3-D Scattergraph Using OpenGL
Introduction
This sample uses OpenGl to create a 3-D Scattergraph that the user can view from any angle by rotating with the mouse. The data can be shown with either orthographic or perspective projection. The data, and optionally the color of each point, are loaded from a text file. The application can be re-sized in the usual manner, and this re-sizes the graph. The slightly unusual feature is that the user can draw around points in the graph to select them, and then zoom the graph so as to magnify the region containing the selected points.
The project is an MFC dialog-based application. The dialog shows all the user-accessed controls, and it contains a CWnd-derived class that encapsulates all the graphing functionality.
In the figure above, the user has selected the cyan-coloured cluster of points by drawing around them after clicking Make in the Selection box. If the Zoom button were to be clicked, the graph would rescale so as to show only those points within the selected cluster. The whole dataset can be shown by selecting the Autoscale options for each dimension.
PLEASE NOTE: I do not claim to be an expert on OpenGL. I have coded this project using the OpenGL Programming Guide (3rd ed), other projects on the CodeGuru site as examples, and a whole load of trial and error. Feedback is welcomed!
Graphing Classes
class COpenGLWnd : public CWnd
This class is a generic base class for projects needing a CWnd-derived class to display OpenGL graphics. It draws heavily on the code architecture for the class GLEnabledView described in the Code Guru article "GL enabled view for MDI environment" by Alessandro Falappa, whose work I gratefully acknowledge. For simplicity I have removed some of the functionality (tessellator and quadric code) contained in that class because it was not needed for this project, but you can easily restore it by consulting that article.
The main additions/changes are as follows:
- COpenGLWnd derives from CWnd, rather than CView, and so can easily be incorporated into non-mdi/sdi projects.
- Optional text support has been added to the class, using the windows-specific wglUseFontBitmaps function. This is a lot easier than mapping out one's own font bitmaps, which seems to be the standard OpenGL method. To activate text support call COpenGLWnd::MakeFont() at some point in the initialisation process. At present this just makes the sysem font available for use, but it would not be hard to extend this to allow user-selection of any available font. To print text call glRasterPos3f to set the printing position, then call COpenGLWnd::PrintString(const char* str) to print the string str.
- GDI drawing support has been added. There is a virtual function COpenGLWnd::OnDrawGDI(CPaintDC *pDC) that gets called from OnPaint, and in which the user can write their own GDI code. I am not sure how robust this is, since the only information that I have been able to find on mixing OpenGL and GDI calls has been that it's not a good idea. However, the method I have used seems to work, at least on my computer...
- The rendered image can be copied to the clipboard as a bitmap.
class CGLMouseRotate : public COpenGLWnd
This class adds the button and mouse code that allows the user to rotate the image by dragging on the screen. A flag determines whether mouse dragging causes rotation or is ignored.
class CGLScatterGraph : public CGLMouseRotate
This class adds the main functionality for drawing the scatter graph.
Data are passed to the class using CGLScatterGraph::SetData(int count,COLORREF
col,float *pCoords, COLORREF *pColList), where count is the number of
points in the graph, col is the colour used to draw the points (only
used if pColList==NULL), pCoords points to an array of floats
containing the x, y and z coordinates of each point (so array length = count*3),
and pColList is either NULL, or points to an array containing the colours
for each point separately.
The array pointers pCoords and pColList have to be maintained by the calling
class, and must not be deleted or go out of scope while the Scattergraph exists,
unless a new call is made to SetData with count = 0.
Only data points whose coordinates fall within the maximum and minimum values for each of the axes are displayed. The axis scales can either be set explicitly by the user, or the graph can autoscale to the maximum and minimum values of any dimension.The graph can be set to display either an orthogonal or a perspective projection.
CGLSelectableScatterGraph : public CGLScatterGraph
This class adds the functionality that allows the user to select points in the graph by drawing around them. Selection mode is entered with a call to CGLSelectableScatterGraph::StartMakeSel(). This disables the mouse rotation facility, and enables a standard drawing routine rather like the original Scribble demo. The class maintains a list of CPoints that describe the shape drawn by the user. The shape can be erased and mouse rotation enabled by a call to CGLSelectableScatterGraph::CancelSel().
At the moment the only use that is made of the selection shape is in the CGLSelectableScatterGraph::ZoomSel() function.
The mapping of the screen shape to the data points is done using the OpenGL feedback mode. The following code shows how this is done:
BOOL CGLSelectableScatterGraph::ZoomSel()
{
int i,j,count;
int id=0;
GLint rv;
GLfloat token;
GLfloat *pBuf;
// m_SelPts is a CArray of CPoints holding the coordinates
// drawn previously by the user
if (m_SelPts.GetSize()>2)
{
// make a region from selPts, need a standard array
CPoint *pt=new CPoint[m_SelPts.GetSize()];
for (i=0; i<m_SelPts.GetSize(); i++)
pt[i]=m_SelPts.GetAt(i);
CRgn rgn;
VERIFY(rgn.CreatePolygonRgn(pt,
m_SelPts.GetSize(),
ALTERNATE));
delete [] pt;
// get a list of coordinates using OpenGL feedback mode
// don't know proper way to calculate how much memory needed
// for feedback buffer, but experiment shows the drawing has
// 96 floats overhead for drawing the axes, + 4 floats per
// point (makes sense as token + 3 coords) give some spare,
// in case miscalculated !!
pBuf=new GLfloat[200+m_Count*4]; // m_Count = number of points
// in the graph
BeginGLCommands(); // a base-class call that just enables the
// OpenGL context
glFeedbackBuffer(200+m_Count*4,GL_3D,pBuf);
glRenderMode(GL_FEEDBACK);
EndGLCommands();
// cause the window to be redrawn, thus calling the GL drawing
// code in feedback mode this puts a list of window coords for
// each item drawn into pBuf, along with a token specifying type
Invalidate();
UpdateWindow();
BeginGLCommands();
rv=glRenderMode(GL_RENDER); // restore standard GL drawing mode
int *pInSel=new int[m_Count]; // for each pt, will be 1 if
// inside, 0 if outside shape need to know the viewport, so can
// invert Y values (since OpenGL Y coords increase upwards, Win
// coords increase downwards)
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT,viewport);
// now find which data points have which screen coords
// code for parsing feedback buffer comes from OpenGL Programming Guide
count=rv;
while (count)
{
token=pBuf[rv-count];
count--;
if (token==GL_POINT_TOKEN) // ignore the graph axis drawing code
{
GLdouble coords[3];
for (j=0; j<3; j++)
{
coords[j]=pBuf[rv-count];
count--;
}
CPoint pt;
pt.x=int(coords[0]);
pt.y=int(viewport[3]-coords[1]-1);
pInSel[id++]=rgn.PtInRegion(pt);
}
}
EndGLCommands();
// now we set the axis scales to draw the selected points
// (plus any others within the axes limits)
m_bAutoScaleX=m_bAutoScaleY=m_bAutoScaleZ=FALSE; // turn off
// autoscaling
// find max & min of pts within selection shape
float xMax,xMin,yMax,yMin,zMax,zMin;
xMax=yMax=zMax=-FLT_MAX;
xMin=yMin=zMin=FLT_MAX;
// drawCount is index of points which were drawn within old axes,
// and which therefore appear in pSelList
int drawCount=-1;
for (i=0; i<m_Count; i++)
{
// determine whether point was being drawn (is within old axes)
// and so will be in selection list (as either 1 if within shape
// or 0 if outside it)
if (!PtWithinAxes(m_pDat[i*3],m_pDat[i*3+1],m_pDat[i*3+2]))
continue; // ignore points outside axes
drawCount++;
if (pInSel[drawCount]==0) // ignore this point, outside
// user selection
continue;
xMax=max(m_pDat[i*3],xMax);
xMin=min(m_pDat[i*3],xMin);
yMax=max(m_pDat[i*3+1],yMax);
yMin=min(m_pDat[i*3+1],yMin);
zMax=max(m_pDat[i*3+2],zMax);
zMin=min(m_pDat[i*3+2],zMin);
}
// prettify axis scales
m_MaxX=NextAbove(xMax,5);
m_MinX=NextBelow(xMin,5);
m_MaxY=NextAbove(yMax,5);
m_MinY=NextBelow(yMin,5);
m_MaxZ=NextAbove(zMax,5);
m_MinZ=NextBelow(zMin,5);
Invalidate();
delete [] pBuf;
delete [] pInSel;
rgn.DeleteObject();
}
m_bAllowMouseRotate=m_bOldAllowRotate; // restore ability to
// rotate using the mouse
CancelSel(); // remove all points from m_SelPts, clear selection
// shape from screen
return TRUE;
}
Data Format
You can load data into the main program either using the Load button, or by dropping a file on the main window. The programme reads text files in any of 3 formats.
In all formats each point occupies a row of the text file.
Format 1: There are 3 tab- or space-separated columns, giving the x, y and z coordinates of the point. With this format the points are all drawn in one colour,
as selected by the user from within the programme.
Format 2: There are 4 columns. The first three give the coordinates, the fourth
is an integer value in the COLORREF range that gives the colour for that point.
Format 3: There are 6 columns. The first three give the coordinates, the next
three give RGB values (range 0-255) for the colour for that point.
The files test1.txt, test2.txt and test3.txt give example data in the three formats.
Some problems
There is sometimes a nasty flicker when resizing the app. If anyone knows how to get rid of it, please let me know!
I tried putting some fog into the graph, which from the book looked like it should be pretty simple to do. But I got weird results - both the back and the front of the scene were fogged, while the mid-distance was clear. I then decided that life was short and that I didn't really want to fog my graph anyway. But it still irritates me, so if anyone actually knows how to get it to work properly, please let me know.

Comments
syntax error
Posted by jag77 on 06/25/2009 12:29pmThe code downloaded and when built, produced numerous warnings and two errors. From file OpenGLWnd.cpp: void COpenGLWnd::StartStockDListDef() { // check if we aren't inside another couple begin/end if(!m_bInsideDispList) { // search a free slot for (int c=0;m_DispListVector[c]!=0;c++); // check if we are inside a drawing session or not.... if(!( m_hRC==wglGetCurrentContext() && m_pCDC->GetSafeHdc()==wglGetCurrentDC() )) { // ...if not specify the target DeviceContext of the subsequent OGL calls wglMakeCurrent(m_pCDC->GetSafeHdc(), m_hRC); // set a warning for EndDispList m_bExternDispListCall=TRUE; }; // create a handle to the disp list (actually an integer) m_DispListVector[c]=glGenLists(1); // set a semaphore m_bInsideDispList=TRUE; // start the disp list: all subsequent OGL calls will be redirected to the list glNewList(m_DispListVector[c],GL_COMPILE); }; } In this line: for (int c=0;m_DispListVector[c]!=0;c++); variable C is declared, but is only local. In these two lines: m_DispListVector[c]=glGenLists(1); glNewList(m_DispListVector[c],GL_COMPILE); It is referenced, but no longer valid. I will move the declaration out of the for loop and see what happens. The original code needs a correction.-
Replyupdate
Posted by jag77 on 06/25/2009 12:36pmI should have compiled and run first. It did compile and run so far as I can tell. Still playing with it. Other than the numerous warnings, looks good. Oh, environment is Windows XP, Visual Studio 2008.
ReplyAxis Color
Posted by leedeblade on 04/10/2009 11:12amSome body know how to make the function change the color of X, Y Z axis and make gird for the coordinate ? Thanks in advance !
ReplyHow to blink a point in scatter plot
Posted by bhanu3_vij on 09/12/2006 04:48amhow to blink a point in a scatter plot. waiting for ur answers. Thank u all.
Replyhow to modify the Scatter3DDlg to Formview?
Posted by mark_maming on 11/14/2005 06:41amYour work is helpful. But I would like to use it as an embeded object, could you tell me how to modify the Scatter3DDlg to Formview? I tried several times but didn't work. Best Regards Mark
ReplyHow to put the left side in a pane and the other side in another pane?
Posted by mark_maming on 11/11/2005 08:51amHow to put the left side in a pane and the other side in another pane? Thanks in advance!
ReplyHow can I embed the set in my propertypage? Thanks in advance
Posted by mark_maming on 10/10/2005 07:04pmMy email address is: mark.ming.ma@gmail.com
-
-
ReplyWilliam, thanks a lot, it's really helpful
Posted by mark_maming on 10/26/2005 04:35amWilliam: thanks a lot, it's really helpful! Best Regards, Mark
Replyputting it in a property page
Posted by William Heitler on 10/26/2005 03:57amPutting the 3D graph into a property page is just like putting it into a standard dialog. In the *.h file you forward declare the class, and define a pointer to an instance: class CGLSelectableScatterGraph; class MyPropPage { ... CGLSelectableScatterGraph m_p3DGraph; ... }; In your resource file draw a frame in the location (e.g. IDC_3D) where you want the graph to appear, but make it invisible. In OnInitDialog of the property page have code something like this ... CRect r; CWnd *pW=GetDlgItem(IDC_3D); pW->GetWindowRect(&r); // top left marker ScreenToClient(r); m_p3DGraph= new CGLSelectableScatterGraph; if (!m_p3DGraph->Create( NULL, //CWnd default NULL, //has no name WS_CHILD|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_VISIBLE, r, this, //this is the parent 1002)) //this should really be a different number... check resource.h { TRACE("Failed to create Histogram window\n"); ASSERT(FALSE); } m_p3DGraph->SetProjection(0); m_p3DGraph->SetSymbolSize(5.f); m_p3DGraph->SetClearCol(RGB(255,255,255)); m_p3DGraph->SetRotationType(1); // some fake data static float fake_data[]= { 0., 0., 0., 1., 1., 1., 0.5, 1, 0., 0.5, 0.5, 0.5 }; static COLORREF fake_colList[] = { RGB(255,0,0), // red RGB(255,255,0), // yellow RGB(0,255,255), // cyan RGB(255,0,255) }; m_p3DGraph->SetData(4,fake_data,fake_colList); ... That should do it.ReplyThank you...
Posted by strgzr on 07/04/2005 04:07amI'm new to OpenGL and find this article very useful to begin with.
ReplyMy thanks
Posted by Legacy on 11/24/2003 12:00amOriginally posted by: Dr. Ian D Wilson
Very useful indeed!
I am using it to generate topological graphs for a range of optimisation functions that will be 'solved' by final year mathematics under-graduates using genetic algorithms.
If you can find the time to add a wire-frame to your scatter-graph I would be very grateful indeed (I simply cannot find the time at the moment).
Best wishes
Ian
ReplyJust What I Needed
Posted by Legacy on 03/28/2003 12:00amOriginally posted by: Thomas Markas
Just what I needed to get into OpenGL on the Windows environment. Its many moon again that I used OpenGL on a Unix box.
ReplyHelp!!! How to change the box size!
Posted by Legacy on 02/06/2003 12:00amOriginally posted by: Min Xu
I have to question need help from you:
1. how to change the box size easily by x, y, z.( eg.x=5cm, y=6cm z=8cm)
2. how to move the image inside the box, up ,down, left, right, maybe controlled by keyboad?
Thanks you very much!
ReplyLoading, Please Wait ...