One of the things I was recently investigating was the ability to generate "gauge" charts—that is, a chart which shows where a value stands within a range of values.
As an example, if we know that, in general, the minimum air pressure is about 45 kPa, and the max is about 150 kPa, then we can generate a "gauge" that shows how far along that trend our current pressure, say 101.325 kPa, is. We might end up with one of the following (depending on whether we want horizontal or arc).
So basically, we can generate one of two graphs, where the value fills the left portion, and indicates, visually, how it stacks up to the range.
The code for each is below, both are mostly identical but have a few small differences.
ArcGauge.fs:
module EBrown.Graphing.ArcGauge
open System.Drawing
open System.Drawing.Drawing2D
type Configuration =
{ Height : int
Width : int
Padding : Rectangle
GaugeWidth : int
Font : Font option
EmptyGaugeColor : Color option
FillGaugeColor : Color option
FontColor : Color option
OutlineColor : Color option
BackgroundColor : Color option
OutlineThickness : float32 option }
let generate<'a> configuration (toFloat : 'a -> float32) (formatter : 'a -> string) (max : 'a) (min : 'a) (value : 'a) =
use defaultFont = new Font("Arial", 12.f, FontStyle.Regular)
let font = configuration.Font |> Option.defaultValue defaultFont
let imageWidth = configuration.Width - configuration.Padding.Top - configuration.Padding.Bottom
let imageHeight = configuration.Height - configuration.Padding.Left - configuration.Padding.Right
let gaugeSweep = 180.0f
let angle = gaugeSweep * (((value |> toFloat) - (min |> toFloat)) / ((max |> toFloat) - (min |> toFloat)))
let gaugeWidth = configuration.GaugeWidth
let startAngle = 180.0f + (180.0f - gaugeSweep) / 2.0f
let image = new Bitmap(imageWidth + configuration.Padding.Left + configuration.Padding.Right, imageHeight + configuration.Padding.Top + configuration.Padding.Bottom)
use g = Graphics.FromImage(image)
g.SmoothingMode <- SmoothingMode.AntiAlias
g.InterpolationMode <- InterpolationMode.HighQualityBicubic
use backgroundBrush = new SolidBrush(configuration.BackgroundColor |> Option.defaultValue Color.White)
use emptyFillBrush = new SolidBrush(configuration.EmptyGaugeColor |> Option.defaultValue (Color.FromArgb(255, 192, 192, 192)))
use gaugeFillBrush = new SolidBrush(configuration.FillGaugeColor |> Option.defaultValue (Color.FromArgb(255, 192, 64, 64)))
use externalPen = new Pen(configuration.OutlineColor |> Option.defaultValue (Color.FromArgb(255, 96, 96, 96)), configuration.OutlineThickness |> Option.defaultValue 1.5f)
use fontBrush = new SolidBrush(configuration.FontColor |> Option.defaultValue Color.Black)
g.FillRectangle(backgroundBrush, Rectangle(0, 0, imageWidth + configuration.Padding.Left + configuration.Padding.Right, imageHeight + configuration.Padding.Top + configuration.Padding.Bottom))
use path = new GraphicsPath()
path.AddArc(Rectangle(configuration.Padding.Left, configuration.Padding.Top, imageWidth, imageWidth), startAngle, angle)
path.Reverse()
path.AddArc(Rectangle(configuration.Padding.Left + gaugeWidth, configuration.Padding.Top + gaugeWidth, imageWidth - gaugeWidth * 2, imageWidth - gaugeWidth * 2), startAngle, angle)
path.CloseFigure()
use externalPath = new GraphicsPath()
externalPath.AddArc(Rectangle(configuration.Padding.Left, configuration.Padding.Top, imageWidth, imageWidth), startAngle, gaugeSweep)
externalPath.Reverse()
externalPath.AddArc(Rectangle(configuration.Padding.Left + gaugeWidth, configuration.Padding.Top + gaugeWidth, imageWidth - gaugeWidth * 2, imageWidth - gaugeWidth * 2), startAngle, gaugeSweep)
externalPath.CloseFigure()
g.FillPath(emptyFillBrush, externalPath)
g.FillPath(gaugeFillBrush, path)
g.DrawPath(externalPen, externalPath)
let drawLabel = General.drawLabelCentered g (Rectangle(0, 0, image.Width, image.Height)) font fontBrush
let gaugeLabelOffsetX = (configuration.Padding.Left |> float32) + (gaugeWidth / 2 |> float32)
let imageMidX = (imageWidth |> float32) * 0.5f + (configuration.Padding.Left |> float32)
drawLabel (value |> formatter) (PointF(imageMidX, (imageWidth |> float32) * 0.5f + (configuration.Padding.Top |> float32) - 10.f))
drawLabel (min |> formatter) (PointF(gaugeLabelOffsetX, (imageWidth |> float32) * 0.5f + 5.f + (configuration.Padding.Top |> float32)))
drawLabel (max |> formatter) (PointF((image.Width |> float32) - gaugeLabelOffsetX, (imageWidth |> float32) * 0.5f + 5.f + (configuration.Padding.Top |> float32)))
image
LinearGauge.fs:
module EBrown.Graphing.LinearGauge
open System.Drawing
open System.Drawing.Drawing2D
type Configuration =
{ Height : int
Width : int
Padding : Rectangle
GaugeWidth : int
Font : Font option
EmptyGaugeColor : Color option
FillGaugeColor : Color option
FontColor : Color option
OutlineColor : Color option
BackgroundColor : Color option
OutlineThickness : float32 option }
let generate<'a> configuration (toFloat : 'a -> float32) (formatter : 'a -> string) (max : 'a) (min : 'a) (value : 'a) =
use defaultFont = new Font("Arial", 12.f, FontStyle.Regular)
let font = configuration.Font |> Option.defaultValue defaultFont
let imageWidth = configuration.Width - configuration.Padding.Top - configuration.Padding.Bottom
let imageHeight = configuration.Height - configuration.Padding.Left - configuration.Padding.Right
let length = ((imageWidth |> float32) * (((value |> toFloat) - (min |> toFloat)) / ((max |> toFloat) - (min |> toFloat)))) |> int
let gaugeWidth = configuration.GaugeWidth
let image = new Bitmap(imageWidth + configuration.Padding.Left + configuration.Padding.Right, imageHeight + configuration.Padding.Top + configuration.Padding.Bottom)
use g = Graphics.FromImage(image)
g.SmoothingMode <- SmoothingMode.AntiAlias
g.InterpolationMode <- InterpolationMode.HighQualityBicubic
use backgroundBrush = new SolidBrush(configuration.BackgroundColor |> Option.defaultValue Color.White)
use emptyFillBrush = new SolidBrush(configuration.EmptyGaugeColor |> Option.defaultValue (Color.FromArgb(255, 192, 192, 192)))
use gaugeFillBrush = new SolidBrush(configuration.FillGaugeColor |> Option.defaultValue (Color.FromArgb(255, 192, 64, 64)))
use externalPen = new Pen(configuration.OutlineColor |> Option.defaultValue (Color.FromArgb(255, 96, 96, 96)), configuration.OutlineThickness |> Option.defaultValue 1.5f)
use fontBrush = new SolidBrush(configuration.FontColor |> Option.defaultValue Color.Black)
g.FillRectangle(backgroundBrush, Rectangle(0, 0, imageWidth + configuration.Padding.Left + configuration.Padding.Right, imageHeight + configuration.Padding.Top + configuration.Padding.Bottom))
let filledGauge = Rectangle(configuration.Padding.Left, configuration.Padding.Top, length, gaugeWidth)
let externalGauge = Rectangle(configuration.Padding.Left, configuration.Padding.Top, imageWidth, gaugeWidth)
g.FillRectangle(emptyFillBrush, externalGauge)
g.FillRectangle(gaugeFillBrush, filledGauge)
g.DrawRectangle(externalPen, externalGauge)
let drawLabel = General.drawLabel g (Rectangle(0, 0, image.Width, image.Height)) font fontBrush
let drawLabelCentered = General.drawLabelCentered g (Rectangle(0, 0, image.Width, image.Height)) font fontBrush
let gaugeLabelOffsetX = (configuration.Padding.Left |> float32)
let imageMidX = (imageWidth |> float32) * 0.5f + (configuration.Padding.Left |> float32)
drawLabelCentered (value |> formatter) (PointF(imageMidX, (configuration.Padding.Top |> float32) + (gaugeWidth |> float32) + 5.f))
drawLabel (min |> formatter) (PointF(gaugeLabelOffsetX, (configuration.Padding.Top |> float32) + (gaugeWidth |> float32) + 5.f))
drawLabel (max |> formatter) (PointF((image.Width |> float32) - gaugeLabelOffsetX - g.MeasureString(max |> formatter, font).Width, (configuration.Padding.Top |> float32) + (gaugeWidth |> float32) + 5.f))
image
You'll notice that both make references to a General
module, which is pretty simple:
General.fs:
module EBrown.Graphing.General
open System.Drawing
let drawLabelCentered (g : Graphics) (bounds : Rectangle) font brush str (ptLoc : PointF) =
let measurements = g.MeasureString(str, font)
g.DrawString(
str,
font,
brush,
PointF(
min (max (ptLoc.X - measurements.Width * 0.5f) (bounds.Left |> float32)) ((bounds.Right |> float32) - measurements.Width),
max (min (ptLoc.Y) ((bounds.Bottom |> float32) - measurements.Height)) ((bounds.Top |> float32))))
let drawLabel (g : Graphics) (bounds : Rectangle) font brush str (ptLoc : PointF) =
let measurements = g.MeasureString(str, font)
g.DrawString(
str,
font,
brush,
PointF(
min (max (ptLoc.X) (bounds.Left |> float32)) ((bounds.Right |> float32) - measurements.Width),
max (min (ptLoc.Y) ((bounds.Bottom |> float32) - measurements.Height)) (bounds.Top |> float32)))
As a test, I have the following F# script:
// Learn more about F# at http://fsharp.org
// See the 'F# Tutorial' project for more help.
#load "General.fs"
#load "ArcGauge.fs"
#load "LinearGauge.fs"
open EBrown.Graphing
// Define your library scripting code here
open System.Drawing
open System.Drawing.Imaging
open System.Diagnostics
let values = (150., 45., 101.325)
let g1() =
let savePath = @"C:\Users\ebrown\Desktop\TestGauge.png"
let padding = Rectangle.FromLTRB(10, 10, 10, 30)
let config =
{ ArcGauge.Configuration.Height = 200
ArcGauge.Configuration.Width = 400
ArcGauge.Configuration.Padding = padding
ArcGauge.Configuration.Font = None
ArcGauge.Configuration.GaugeWidth = 64
ArcGauge.Configuration.EmptyGaugeColor = Color.FromArgb(255, 192, 192, 192) |> Some
ArcGauge.Configuration.FillGaugeColor = Color.FromArgb(255, 96, 16, 16) |> Some
ArcGauge.Configuration.FontColor = Color.FromArgb(255, 32, 0, 0) |> Some
ArcGauge.Configuration.OutlineColor = Color.FromArgb(255, 64, 64, 64) |> Some
ArcGauge.Configuration.BackgroundColor = Color.White |> Some
ArcGauge.Configuration.OutlineThickness = 1.25f |> Some }
let image = values|||> ArcGauge.generate config float32 (fun f -> f.ToString("0 KPa"))
image.Save(savePath, ImageFormat.Png)
Process.Start savePath |> ignore
let g2() =
let savePath = @"C:\Users\ebrown\Desktop\TestGauge2.png"
let padding = Rectangle.FromLTRB(10, 10, 10, 30)
let config =
{ LinearGauge.Configuration.Height = 50
LinearGauge.Configuration.Width = 400
LinearGauge.Configuration.Padding = padding
LinearGauge.Configuration.Font = None
LinearGauge.Configuration.GaugeWidth = 32
LinearGauge.Configuration.EmptyGaugeColor = Color.FromArgb(255, 192, 192, 192) |> Some
LinearGauge.Configuration.FillGaugeColor = Color.FromArgb(255, 96, 16, 16) |> Some
LinearGauge.Configuration.FontColor = Color.FromArgb(255, 32, 0, 0) |> Some
LinearGauge.Configuration.OutlineColor = Color.FromArgb(255, 64, 64, 64) |> Some
LinearGauge.Configuration.BackgroundColor = Color.White |> Some
LinearGauge.Configuration.OutlineThickness = 1.25f |> Some }
let image = values|||> LinearGauge.generate config float32 (fun f -> f.ToString("0 KPa"))
image.Save(savePath, ImageFormat.Png)
Process.Start savePath |> ignore
//() |> g1
//() |> g2
Lastly, you can find this on GitHub.
open EBrown.Graphing.General
does not compile. The value, namespace, type or module 'General' is not defined. \$\endgroup\$