Add support to analyse driveresponse for drawing its own graphs via AGG.

This commit is contained in:
David Given
2021-01-13 01:46:48 +01:00
parent 03fc1419de
commit 57e598156c
5 changed files with 238 additions and 268 deletions

View File

@@ -22,9 +22,6 @@
#ifndef AGG2D_INCLUDED
#define AGG2D_INCLUDED
// With this define uncommented you can use FreeType font engine
//#define AGG2D_USE_FREETYPE
// With this define uncommented you can use floating-point pixel format
//#define AGG2D_USE_FLOAT_FORMAT
@@ -50,18 +47,9 @@
#include "agg_rounded_rect.h"
#include "agg_font_cache_manager.h"
// Font drawing is deactivated here because the implementation is platform-dependent and incomplete.
//#define AGG_USE_FONTS
#ifdef AGG_USE_FONTS
# ifdef AGG2D_USE_FREETYPE
# include "agg_font_freetype.h"
# else
# include "agg_font_win32_tt.h"
# endif
#endif
#include "agg_pixfmt_rgba.h"
#include "agg_image_accessors.h"
#include <string>
class Agg2D
{
@@ -95,16 +83,6 @@ class Agg2D
typedef agg::span_gradient<ColorType, agg::span_interpolator_linear<>, agg::gradient_x, GradientArray> LinearGradientSpan;
typedef agg::span_gradient<ColorType, agg::span_interpolator_linear<>, agg::gradient_circle, GradientArray> RadialGradientSpan;
#ifdef AGG_USE_FONTS
# ifdef AGG2D_USE_FREETYPE
typedef agg::font_engine_freetype_int32 FontEngine;
# else
typedef agg::font_engine_win32_tt_int32 FontEngine;
# endif
typedef agg::font_cache_manager<FontEngine> FontCacheManager;
typedef FontCacheManager::gray8_adaptor_type FontRasterizer;
typedef FontCacheManager::gray8_scanline_type FontScanline;
#endif
typedef agg::conv_curve<agg::path_storage> ConvCurve;
typedef agg::conv_stroke<ConvCurve> ConvStroke;
typedef agg::conv_transform<ConvCurve> PathTransform;
@@ -147,8 +125,6 @@ public:
AlignLeft,
AlignRight,
AlignCenter,
AlignTop = AlignRight,
AlignBottom = AlignLeft
};
@@ -337,6 +313,10 @@ public:
void fillEvenOdd(bool evenOddFlag);
bool fillEvenOdd() const;
void textAlignment(TextAlignment alignment);
void textSize(double sizeX, double sizeY);
inline void textSize(double size) { textSize(size, size); }
// Transformations
//-----------------------
Transformations transformations() const;
@@ -371,23 +351,6 @@ public:
void polygon(double* xy, int numPoints);
void polyline(double* xy, int numPoints);
#ifdef AGG_USE_FONTS
// Text
//-----------------------
void flipText(bool flip);
void font(const char* fileName, double height,
bool bold = false,
bool italic = false,
FontCacheType ch = RasterFontCache,
double angle = 0.0);
double fontHeight() const;
void textAlignment(TextAlignment alignX, TextAlignment alignY);
bool textHints() const;
void textHints(bool hints);
double textWidth(const char* str);
void text(double x, double y, const char* str, bool roundOff=false, double dx=0.0, double dy=0.0);
#endif
// Path commands
//-----------------------
void resetPath();
@@ -438,6 +401,7 @@ public:
double xTo, double yTo);
void addEllipse(double cx, double cy, double rx, double ry, Direction dir);
void text(double x, double y, const std::string& text);
void closePolygon();
void drawPath(DrawPathFlag flag = FillAndStroke);
@@ -552,14 +516,9 @@ private:
double m_fillGradientD2;
double m_lineGradientD2;
double m_textAngle;
TextAlignment m_textAlignX;
TextAlignment m_textAlignY;
bool m_textHints;
double m_fontHeight;
double m_fontAscent;
double m_fontDescent;
FontCacheType m_fontCacheType;
TextAlignment m_textAlignment;
double m_textSizeX;
double m_textSizeY;
ImageFilter m_imageFilter;
ImageResample m_imageResample;

View File

@@ -172,6 +172,9 @@ namespace agg
//--------------------------------------------------------------------
static rgba from_wavelength(double wl, double gamma = 1.0);
//--------------------------------------------------------------------
static rgba from_hsv(double h, double s, double v);
//--------------------------------------------------------------------
explicit rgba(double wavelen, double gamma=1.0)
{
@@ -235,6 +238,30 @@ namespace agg
return t;
}
inline rgba rgba::from_hsv(double h, double s, double v)
{
h = fmod(h, 360.0);
double hh = h / 60.0;
int i = (int)hh;
double ff = hh - i;
double p = v * (1.0 - s);
double q = v * (1.0 - s*ff);
double t = v * (1.0 - s*(1.0 - ff));
double r, g, b;
switch (i) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5:
default: r = v; g = p; b = q; break;
}
return rgba(r, g, b, 1.0);
}
inline rgba rgba_pre(double r, double g, double b, double a)
{
return rgba(r, g, b, a).premultiply();

View File

@@ -24,6 +24,7 @@
//----------------------------------------------------------------------------
#include "agg2d.h"
#include "agg_gsv_text.h"
static const double g_approxScale = 2.0;
@@ -79,14 +80,9 @@ Agg2D::Agg2D() :
m_fillGradientD2(100.0),
m_lineGradientD2(100.0),
m_textAngle(0.0),
m_textAlignX(AlignLeft),
m_textAlignY(AlignBottom),
m_textHints(true),
m_fontHeight(0.0),
m_fontAscent(0.0),
m_fontDescent(0.0),
m_fontCacheType(RasterFontCache),
m_textAlignment(AlignLeft),
m_textSizeX(10.0),
m_textSizeY(10.0),
m_imageFilter(Bilinear),
m_imageResample(NoResample),
@@ -138,15 +134,9 @@ void Agg2D::attach(unsigned char* buf, unsigned width, unsigned height, int stri
lineWidth(1.0),
lineColor(0,0,0);
fillColor(255,255,255);
#ifdef AGG_USE_FONTS
textAlignment(AlignLeft, AlignBottom);
#endif
clipBox(0, 0, width, height);
lineCap(CapRound);
lineJoin(JoinRound);
#ifdef AGG_USE_FONTS
flipText(false);
#endif
imageFilter(Bilinear);
imageResample(NoResample);
m_masterAlpha = 1.0;
@@ -908,183 +898,19 @@ void Agg2D::polyline(double* xy, int numPoints)
drawPath(StrokeOnly);
}
#ifdef AGG_USE_FONTS
//------------------------------------------------------------------------
void Agg2D::flipText(bool flip)
void Agg2D::textSize(double sizeX, double sizeY)
{
m_fontEngine.flip_y(flip);
m_textSizeX = sizeX;
m_textSizeY = sizeY;
}
//------------------------------------------------------------------------
void Agg2D::font(const char* fontName,
double height,
bool bold,
bool italic,
FontCacheType ch,
double angle)
void Agg2D::textAlignment(TextAlignment alignment)
{
m_textAngle = angle;
m_fontHeight = height;
m_fontCacheType = ch;
#ifdef AGG2D_USE_FREETYPE
m_fontEngine.load_font(fontName,
0,
(ch == VectorFontCache) ?
agg::glyph_ren_outline :
agg::glyph_ren_agg_gray8);
m_fontEngine.hinting(m_textHints);
m_fontEngine.height((ch == VectorFontCache) ? height : worldToScreen(height));
#else
m_fontEngine.hinting(m_textHints);
m_fontEngine.create_font(fontName,
(ch == VectorFontCache) ?
agg::glyph_ren_outline :
agg::glyph_ren_agg_gray8,
(ch == VectorFontCache) ? height : worldToScreen(height),
0.0,
bold ? 700 : 400,
italic);
#endif
m_textAlignment = alignment;
}
//------------------------------------------------------------------------
double Agg2D::fontHeight() const
{
return m_fontHeight;
}
//------------------------------------------------------------------------
void Agg2D::textAlignment(TextAlignment alignX, TextAlignment alignY)
{
m_textAlignX = alignX;
m_textAlignY = alignY;
}
//------------------------------------------------------------------------
double Agg2D::textWidth(const char* str)
{
double x = 0;
double y = 0;
bool first = true;
while(*str)
{
const agg::glyph_cache* glyph = m_fontCacheManager.glyph(*str);
if(glyph)
{
if(!first) m_fontCacheManager.add_kerning(&x, &y);
x += glyph->advance_x;
y += glyph->advance_y;
first = false;
}
++str;
}
return (m_fontCacheType == VectorFontCache) ? x : screenToWorld(x);
}
//------------------------------------------------------------------------
bool Agg2D::textHints() const
{
return m_textHints;
}
//------------------------------------------------------------------------
void Agg2D::textHints(bool hints)
{
m_textHints = hints;
}
//------------------------------------------------------------------------
void Agg2D::text(double x, double y, const char* str, bool roundOff, double ddx, double ddy)
{
double dx = 0.0;
double dy = 0.0;
switch(m_textAlignX)
{
case AlignCenter: dx = -textWidth(str) * 0.5; break;
case AlignRight: dx = -textWidth(str); break;
default: break;
}
double asc = fontHeight();
const agg::glyph_cache* glyph = m_fontCacheManager.glyph('H');
if(glyph)
{
asc = glyph->bounds.y2 - glyph->bounds.y1;
}
if(m_fontCacheType == RasterFontCache)
{
asc = screenToWorld(asc);
}
switch(m_textAlignY)
{
case AlignCenter: dy = -asc * 0.5; break;
case AlignTop: dy = -asc; break;
default: break;
}
if(m_fontEngine.flip_y()) dy = -dy;
agg::trans_affine mtx;
double start_x = x + dx;
double start_y = y + dy;
if (roundOff)
{
start_x = int(start_x);
start_y = int(start_y);
}
start_x += ddx;
start_y += ddy;
mtx *= agg::trans_affine_translation(-x, -y);
mtx *= agg::trans_affine_rotation(m_textAngle);
mtx *= agg::trans_affine_translation(x, y);
agg::conv_transform<FontCacheManager::path_adaptor_type> tr(m_fontCacheManager.path_adaptor(), mtx);
if(m_fontCacheType == RasterFontCache)
{
worldToScreen(start_x, start_y);
}
int i;
for (i = 0; str[i]; i++)
{
glyph = m_fontCacheManager.glyph(str[i]);
if(glyph)
{
if(i) m_fontCacheManager.add_kerning(&start_x, &start_y);
m_fontCacheManager.init_embedded_adaptors(glyph, start_x, start_y);
if(glyph->data_type == agg::glyph_data_outline)
{
m_path.remove_all();
//m_path.add_path(tr, 0, false);
m_path.concat_path(tr,0); // JME
drawPath();
}
if(glyph->data_type == agg::glyph_data_gray8)
{
render(m_fontCacheManager.gray8_adaptor(),
m_fontCacheManager.gray8_scanline());
}
start_x += glyph->advance_x;
start_y += glyph->advance_y;
}
}
}
#endif // AGG_USE_FONTS
//------------------------------------------------------------------------
void Agg2D::resetPath() { m_path.remove_all(); }
@@ -1237,6 +1063,33 @@ void Agg2D::addEllipse(double cx, double cy, double rx, double ry, Direction dir
m_path.close_polygon();
}
//------------------------------------------------------------------------
void Agg2D::text(double x, double y, const std::string& text)
{
agg::gsv_text t;
t.flip(true);
t.size(m_textSizeX, m_textSizeY);
t.text(text.c_str());
double w = t.text_width();
switch (m_textAlignment)
{
case AlignLeft:
break;
case AlignRight:
x -= w;
break;
case AlignCenter:
x -= w/2.0;
break;
}
t.start_point(x, y);
m_path.concat_path(t);
drawPath();
}
//------------------------------------------------------------------------
void Agg2D::closePolygon()
{

View File

@@ -25,7 +25,7 @@
namespace agg
{
int8u gsv_default_font[] =
const int8u gsv_default_font[] =
{
0x40,0x00,0x6c,0x0f,0x15,0x00,0x0e,0x00,0xf9,0xff,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,

View File

@@ -45,28 +45,137 @@ static DataSpecFlag writeImg(
"Draw a graph of the response data",
":w=640:h=480");
static agg::srgba8 hsvToRgb(double h, double s, double v)
/* This is the Turbo colourmap.
* https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html
*/
static const uint8_t turbo_srgb_bytes[256][3] = {
{48,18,59}, {50,21,67}, {51,24,74}, {52,27,81}, {53,30,88}, {54,33,95},
{55,36,102}, {56,39,109}, {57,42,115}, {58,45,121}, {59,47,128},
{60,50,134}, {61,53,139}, {62,56,145}, {63,59,151}, {63,62,156},
{64,64,162}, {65,67,167}, {65,70,172}, {66,73,177}, {66,75,181},
{67,78,186}, {68,81,191}, {68,84,195}, {68,86,199}, {69,89,203},
{69,92,207}, {69,94,211}, {70,97,214}, {70,100,218}, {70,102,221},
{70,105,224}, {70,107,227}, {71,110,230}, {71,113,233}, {71,115,235},
{71,118,238}, {71,120,240}, {71,123,242}, {70,125,244}, {70,128,246},
{70,130,248}, {70,133,250}, {70,135,251}, {69,138,252}, {69,140,253},
{68,143,254}, {67,145,254}, {66,148,255}, {65,150,255}, {64,153,255},
{62,155,254}, {61,158,254}, {59,160,253}, {58,163,252}, {56,165,251},
{55,168,250}, {53,171,248}, {51,173,247}, {49,175,245}, {47,178,244},
{46,180,242}, {44,183,240}, {42,185,238}, {40,188,235}, {39,190,233},
{37,192,231}, {35,195,228}, {34,197,226}, {32,199,223}, {31,201,221},
{30,203,218}, {28,205,216}, {27,208,213}, {26,210,210}, {26,212,208},
{25,213,205}, {24,215,202}, {24,217,200}, {24,219,197}, {24,221,194},
{24,222,192}, {24,224,189}, {25,226,187}, {25,227,185}, {26,228,182},
{28,230,180}, {29,231,178}, {31,233,175}, {32,234,172}, {34,235,170},
{37,236,167}, {39,238,164}, {42,239,161}, {44,240,158}, {47,241,155},
{50,242,152}, {53,243,148}, {56,244,145}, {60,245,142}, {63,246,138},
{67,247,135}, {70,248,132}, {74,248,128}, {78,249,125}, {82,250,122},
{85,250,118}, {89,251,115}, {93,252,111}, {97,252,108}, {101,253,105},
{105,253,102}, {109,254,98}, {113,254,95}, {117,254,92}, {121,254,89},
{125,255,86}, {128,255,83}, {132,255,81}, {136,255,78}, {139,255,75},
{143,255,73}, {146,255,71}, {150,254,68}, {153,254,66}, {156,254,64},
{159,253,63}, {161,253,61}, {164,252,60}, {167,252,58}, {169,251,57},
{172,251,56}, {175,250,55}, {177,249,54}, {180,248,54}, {183,247,53},
{185,246,53}, {188,245,52}, {190,244,52}, {193,243,52}, {195,241,52},
{198,240,52}, {200,239,52}, {203,237,52}, {205,236,52}, {208,234,52},
{210,233,53}, {212,231,53}, {215,229,53}, {217,228,54}, {219,226,54},
{221,224,55}, {223,223,55}, {225,221,55}, {227,219,56}, {229,217,56},
{231,215,57}, {233,213,57}, {235,211,57}, {236,209,58}, {238,207,58},
{239,205,58}, {241,203,58}, {242,201,58}, {244,199,58}, {245,197,58},
{246,195,58}, {247,193,58}, {248,190,57}, {249,188,57}, {250,186,57},
{251,184,56}, {251,182,55}, {252,179,54}, {252,177,54}, {253,174,53},
{253,172,52}, {254,169,51}, {254,167,50}, {254,164,49}, {254,161,48},
{254,158,47}, {254,155,45}, {254,153,44}, {254,150,43}, {254,147,42},
{254,144,41}, {253,141,39}, {253,138,38}, {252,135,37}, {252,132,35},
{251,129,34}, {251,126,33}, {250,123,31}, {249,120,30}, {249,117,29},
{248,114,28}, {247,111,26}, {246,108,25}, {245,105,24}, {244,102,23},
{243,99,21}, {242,96,20}, {241,93,19}, {240,91,18}, {239,88,17},
{237,85,16}, {236,83,15}, {235,80,14}, {234,78,13}, {232,75,12},
{231,73,12}, {229,71,11}, {228,69,10}, {226,67,10}, {225,65,9},
{223,63,8}, {221,61,8}, {220,59,7}, {218,57,7}, {216,55,6}, {214,53,6},
{212,51,5}, {210,49,5}, {208,47,5}, {206,45,4}, {204,43,4}, {202,42,4},
{200,40,3}, {197,38,3}, {195,37,3}, {193,35,2}, {190,33,2}, {188,32,2},
{185,30,2}, {183,29,2}, {180,27,1}, {178,26,1}, {175,24,1}, {172,23,1},
{169,22,1}, {167,20,1}, {164,19,1}, {161,18,1}, {158,16,1}, {155,15,1},
{152,14,1}, {149,13,1}, {146,11,1}, {142,10,1}, {139,9,2}, {136,8,2},
{133,7,2}, {129,6,2}, {126,5,2}, {122,4,3}};
static void palette(double value, agg::srgba8* pixel)
{
h = fmod(h, 360.0);
double hh = h / 60.0;
int i = (int)hh;
double ff = hh - i;
double p = v * (1.0 - s);
double q = v * (1.0 - s*ff);
double t = v * (1.0 - s*(1.0 - ff));
int index = std::min((int)(value * 256.0), 255);
pixel->r = turbo_srgb_bytes[index][0];
pixel->g = turbo_srgb_bytes[index][1];
pixel->b = turbo_srgb_bytes[index][2];
pixel->a = 255;
}
double r, g, b;
switch (i) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5:
default: r = v; g = p; b = q; break;
}
static void do_in_steps(double c1, double c2, double lo, double hi,
double step, std::function<void(double c, double v)> func)
{
double scale = (c2 - c1) / (hi - lo);
double v = lo;
while (v <= hi + step/10.0)
{
double c = c1 + scale*(v - lo);
func(c, v);
v += step;
}
}
return agg::srgba8(b*255.0, g*255.0, r*255.0, 255);
static void draw_y_axis(Agg2D& painter, double x, double y1, double y2,
double lo, double hi, double step, const char* format)
{
painter.noFill();
painter.lineColor(0, 0, 0);
painter.line(x, y1, x, y2);
painter.textSize(10.0);
painter.textAlignment(Agg2D::AlignRight);
do_in_steps(y1, y2, lo, hi, step, [&](double y, double v)
{
painter.line(x, y, x-5, y);
painter.text(x-8, y+5.0, fmt::format(format, v));
});
}
static void draw_x_axis(Agg2D& painter, double x1, double x2, double y,
double lo, double hi, double step, const char* format)
{
painter.noFill();
painter.lineColor(0, 0, 0);
painter.line(x1, y, x2, y);
painter.textSize(10.0);
painter.textAlignment(Agg2D::AlignCenter);
do_in_steps(x1, x2, lo, hi, step, [&](double x, double v)
{
painter.line(x, y, x, y+5);
painter.text(x, y+18, fmt::format(format, v));
});
}
static void draw_y_graticules(Agg2D& painter, double x1, double y1, double x2, double y2,
double lo, double hi, double step)
{
painter.noFill();
painter.lineColor(0, 0, 0, 128);
do_in_steps(y1, y2, lo, hi, step, [&](double y, double v)
{
painter.line(x1, y, x2, y);
});
}
static void draw_x_graticules(Agg2D& painter, double x1, double y1, double x2, double y2,
double lo, double hi, double step)
{
painter.noFill();
painter.lineColor(0, 0, 0, 128);
do_in_steps(x1, x2, lo, hi, step, [&](double x, double v)
{
painter.line(x, y1, x, y2);
});
}
int mainAnalyseDriveResponse(int argc, const char* argv[])
@@ -91,7 +200,7 @@ int mainAnalyseDriveResponse(int argc, const char* argv[])
int numRows = (maxInterval - minInterval) / intervalStep;
const int numColumns = 512;
double frequencies[numRows][numColumns] = {};
double frequencies[numRows+1][numColumns] = {};
int row = 0;
for (double interval = minInterval; interval<maxInterval; interval += intervalStep, row++)
@@ -169,38 +278,60 @@ int mainAnalyseDriveResponse(int argc, const char* argv[])
Agg2D& painter = bitmapSpec.painter();
painter.clearAll(0xdd, 0xdd, 0xdd);
const double MARGIN = 20;
const double MARGIN = 30;
agg::rect_d drawableBounds = {
MARGIN, MARGIN,
MARGIN*1.5, MARGIN,
bitmapSpec.width - MARGIN, bitmapSpec.height - MARGIN
};
agg::rect_d colourbarBounds = {
drawableBounds.x2 - MARGIN*4, drawableBounds.y1,
drawableBounds.x2 - MARGIN, drawableBounds.y1,
drawableBounds.x2, drawableBounds.y2
};
agg::rect_d graphBounds = {
drawableBounds.x1, drawableBounds.y1,
colourbarBounds.x1, drawableBounds.y2
colourbarBounds.x1 - MARGIN*2, drawableBounds.y2
};
double blockWidth = (graphBounds.x2 - graphBounds.x1) / numColumns;
double blockHeight = (graphBounds.y2 - graphBounds.y1) / numRows;
painter.imageFilter(Agg2D::NoFilter);
painter.imageResample(Agg2D::NoResample);
painter.imageBlendMode(Agg2D::BlendDst);
/* Create the off-screen buffer which the actual bitmap goes into, and draw it. */
painter.noLine();
for (int x=0; x<numColumns; x++)
{
for (int y=0; y<numRows; y++)
{
double xx = graphBounds.x1 + x*blockWidth;
double yy = graphBounds.y2 - y*blockHeight;
const int width = numRows; /* input interval on X axis */
const int height = numColumns; /* response spread on Y axis */
agg::srgba8 rbufdata[height][width];
for (int y=0; y<height; y++)
for (int x=0; x<width; x++)
palette(frequencies[x][y], &rbufdata[y][x]);
painter.fillColor(hsvToRgb(360.0 * ((double)x/numColumns), 1.0, 1.0));
painter.rectangle(xx, yy-blockHeight, xx+blockWidth, yy);
}
Agg2D::Image image((uint8_t*)&rbufdata[0][0], width, height, width*sizeof(agg::srgba8));
painter.transformImage(image, graphBounds.x1, graphBounds.y2, graphBounds.x2, graphBounds.y1);
}
/* Likewise for the colour bar. */
{
const int HEIGHT = graphBounds.y2 - graphBounds.y1;
agg::srgba8 rbufdata[HEIGHT];
for (int y=0; y<HEIGHT; y++)
palette((double)y / HEIGHT, &rbufdata[y]);
Agg2D::Image image((uint8_t*)&rbufdata[0], 1, HEIGHT, sizeof(agg::srgba8));
painter.transformImage(image, colourbarBounds.x1, colourbarBounds.y2, colourbarBounds.x2, colourbarBounds.y1);
}
draw_y_axis(painter, colourbarBounds.x1-5, colourbarBounds.y2, colourbarBounds.y1, 0.0, 1.0, 0.1, "{:.1f}");
draw_y_axis(painter, graphBounds.x1-5, graphBounds.y2, graphBounds.y1, 0.0, 512.0/TICKS_PER_US, 5.0, "{:.0f}");
draw_y_graticules(painter, graphBounds.x1, graphBounds.y2, graphBounds.x2, graphBounds.y1, 0.0, 512.0/TICKS_PER_US, 5.0);
draw_x_axis(painter, graphBounds.x1, graphBounds.x2, graphBounds.y2+5, minInterval, maxInterval, 5.0, "{:.0f}");
draw_x_graticules(painter, graphBounds.x1, graphBounds.y1, graphBounds.x2, graphBounds.y2, minInterval, maxInterval, 5.0);
painter.noFill();
painter.lineColor(0, 0, 0);
painter.rectangle(graphBounds.x1, drawableBounds.y1, drawableBounds.x2, drawableBounds.y2);
painter.rectangle(graphBounds.x1, graphBounds.y1, graphBounds.x2, graphBounds.y2);
painter.rectangle(colourbarBounds.x1, drawableBounds.y1, drawableBounds.x2, drawableBounds.y2);
bitmapSpec.save();
}