Blog

All Blog Posts  |  Next Post  |  Previous Post

Extend TMS WEB Core with JS Libraries with Andrew:
Charting Part 1: Sparklines

Tuesday, August 16, 2022

Photo of Andrew Simard

Now that we've covered integrating images, maps, and diagrams into our TMS WEB Core projects, the next area to explore is charts. A big topic, to be sure. And with no shortage of options, either. There are numerous JavaScript libraries available that are quite capable in this regard, including of course TMS FNC Charts. Whether your charting needs are big or small, there is likely to be a charting tool well-suited to the task and even a few that are well-suited to nearly any task. But for this miniseries, we're going to have a look at a few scenarios where a more specific approach may be helpful. For example, we're going to start with Sparklines - very small charts where a full-blown charting package might be a little overwhelming.


Motivation.

Charts are a natural extension of data. As soon as you have to display more than a few numbers, having a more visual representation of the data may suddenly become very useful. But there are many situations where that representation can be extremely simple, which is where Sparklines come in. Sparklines are defined (according to Wikipedia) as "a very small line chart, typically drawn without axes or coordinates". One way to quantify that a bit further is to imagine a chart small enough to appear inline within a passage of normal text. For our purposes, we'll relax that definition a bit and just assume that it is a chart of any kind that typically displays only a single series of data, without any annotations (text) of any kind, and without any kind of interaction. Nothing stopping you from doing more than that, but that's roughly what we're expecting to work with when we think of Sparklines.

To implement Sparklines in our TMS WEB Core projects, one option is to use a JavaScript library. For example, Peity Vanilla, which is, as its name implies, a Vanilla JavaScript library closely modeled after Peity, a popular jQuery Sparklines library. If you're already using jQuery in your project, that may also be a good choice, or perhaps even better. And there are many other contenders, so if neither of these is particularly appealing style-wise, then the same approaches we're taking here to integrate them into TMS WEB Core projects could very likely apply equally well to a handful of similar JavaScript libraries. The attraction to this particular choice was primarily that (1) SVG images are created, which are easy to scale and otherwise work with and (2) support for a few other chart types (bar, pie, and donut charts) is also included.


Getting Started.

As usual, we can add a link to our Project.html from a CDN or add a link via the Manage JavaScript Libraries function of the Delphi IDE. In this case, there's only a single entry for the JavaScript library, as no CSS is required.


<script src="https://cdn.jsdelivr.net/npm/peity-vanilla@0.0.8/dist/peity-vanilla.min.js"></script>


To see it in action, we'll create the usual new TMS WEB Bootstrap Application using the available template. And then add a TWebHTMLDiv component for each chart that we'd like to display. Let's put these on a background of some kind.

There are many approaches one might take in handling the numerous potential parameters, which are different for each type of Sparkline chart. One is to just pass them as parameters to a Delphi function, and just have different functions for each chart type, which is what we're going to do here, starting with the "line" type. 

Depending on how many different charts and chart styles your project needs, you might want to define functions that have many of these parameters set separately, so you don't have to pass all of them all of the time.


procedure TForm1.Sparkline_Line(Chart: TWebHTMLDiv; ChartData: String; MinRange: String; MaxRange: String; Fill: String; Stroke: String; StrokeWidth: Integer);
var
  Element: TJSElement;
  Width: Integer;
  Height: Integer;

begin
  // Create a place to attach the Sparkline
  Chart.ElementHandle.innerHTML := '<span></span>';

  // Add data to this place
  Element := Chart.ElementHandle.firstElementChild;
  Element.innerHTML := ChartData;

  // Get dimensions from size of encompassing DIV
  Width := Chart.Width;
  Height := Chart.Height;

  asm
    if (MinRange == 'auto') {
      var arr =JSON.parse('['+ChartData+']');
      MinRange = Math.min(...arr);
    }
    if (MaxRange == 'auto') {
      var arr =JSON.parse('['+ChartData+']');
      MaxRange = Math.max(...arr);
    }

    peity(Element, "line", {
      width: Width,
      height: Height,
      min: MinRange,
      max: MaxRange,
      fill: Fill,
      stroke: Stroke,
      strokeWidth: StrokeWidth
    });

  end;
end;

procedure TForm1.WebButton1Click(Sender: TObject);
begin
  Sparkline_Line(
    LineChart1,       // TWebHTMLDiv
    '8,6,7,5,3,0,9',  // Data
    '0',              // Minimum
    '20',             // Maximum
    'transparent',    // Fill
    'yellow',         // Stroke
    2                 // StrokeWidth
  );

  Sparkline_Line(
    LineChart2,       // TWebHTMLDiv
    '8,6,7,5,3,0,9',  // Data
    '0',              // Minimum
    '30',             // Maximum
    'transparent',    // Fill
    'rgb(99,99,99)',  // Stroke
    3                 // StrokeWidth
  );

  Sparkline_Line(
    LineChart3,       // TWebHTMLDiv
    '8,6,7,5,3,0,9',  // Data
    'auto',           // Minimum
    'auto',           // Maximum
    '#7777FF',        // Fill
    'blue',           // Stroke
    5                 // StrokeWidth
  );

end;


Passing the same set of data, but adjusting the parameters, gives us something like the following.  Note the variations in how colors are being passed. Everything but Delphi TColors, but this is something that could be handled without too much trouble. Also, we're changing the max values, which causes the chart to flatten (or not) depending on the max value in the data.


TMS Software Delphi  Components
Sparkline "Line" Examples.

Drawing bar charts involves basically the same approach but with the added "padding" parameter instead of the Stroke parameters. Not entirely sure what the units are here, but a small decimal number seems to be called for, with the default being 0.1. Also, the color (fill) is passed as an array, where adding colors allows for having bars of different colors in the same chart, which are used in rotation if the number of bars exceeds the number of colors in the array. A function can also be used if there are more complex criteria, like changing bars in a certain value range to a certain color.


procedure TForm1.Sparkline_Bar(Chart: TWebHTMLDiv; ChartData: String; MinRange: String; MaxRange: String; Fill: String; Padding: Double);
var
  Element: TJSElement;
  Width: Integer;
  Height: Integer;

begin
  // Create a place to attach the Sparkline
  Chart.ElementHandle.innerHTML := '<span></span>';

  // Add data to this place
  Element := Chart.ElementHandle.firstElementChild;
  Element.innerHTML := ChartData;

  // Get dimensions from size of encompassing DIV
  Width := Chart.Width;
  Height := Chart.Height;

  asm
    if (MinRange == 'auto') {
      var arr =JSON.parse('['+ChartData+']');
      MinRange = Math.min(...arr);
    }
    if (MaxRange == 'auto') {
      var arr =JSON.parse('['+ChartData+']');
      MaxRange = Math.max(...arr);
    }
    console.log('Min:'+MinRange+' Max:'+MaxRange);
    peity(Element, "bar", {
      width: Width,
      height: Height,
      min: MinRange,
      max: MaxRange,
      fill: JSON.parse(Fill),
      padding: Padding
    });

  end;
end;

procedure TForm1.WebButton1Click(Sender: TObject);
begin
  Sparkline_Bar(
    BarChart1,        // TWebHTMLDiv
    '8,6,7,5,3,0,9',  // Data
    '0',              // Minimum
    '20',             // Maximum
    '["yellow"]',       // Fill
    0.2               // Padding
  );

  Sparkline_Bar(
    BarChart2,           // TWebHTMLDiv
    '8,6,7,5,3,0,9',     // Data
    '0',                 // Minimum
    '30',                // Maximum
    '["rgb(99,99,99)"]', // Fill
    0.3                  // Padding
  );

  Sparkline_Bar(
    BarChart3,           // TWebHTMLDiv
    '8,6,7,5,3,0,9',     // Data
    'auto',              // Minimum
    'auto',              // Maximum
    '["#7777FF"]',       // Fill
    0.4                  // Padding
  );

end;


For now, using the same set of data, we get similar bar charts.


TMS Software Delphi  Components
Sparkline "Bar" Examples.

While pie charts aren't normally what comes to mind when we think of Sparklines, they work just as well. Here, we have a few ways to pass data. Either as a ratio (A/B) or as a list of slice sizes, where the total is determined by the sum of the pieces. Nothing really fancy or complex here, no pie pieces extending out or anything like that. Just a pie chart, plain and simple. As with the bar colors, the colors passed here are used to indicate the slice colors in the same order. For extra credit, we've also got an additional parameter to set the rotation, in case you don't want the first slice to start at the top.


procedure TForm1.Sparkline_Pie(Chart: TWebHTMLDiv; ChartData: String; Fill: String; Rotation: String);
var
  Element: TJSElement;
  Width: Integer;
  Height: Integer;

begin
  // Create a place to attach the Sparkline
  Chart.ElementHandle.innerHTML := '<span></span>';

  // Add data to this place
  Element := Chart.ElementHandle.firstElementChild;
  Element.innerHTML := ChartData;

  // Get dimensions from size of encompassing DIV
  Width := Chart.Width;
  Height := Chart.Height;

  asm
    peity(Element, "pie", {
      width: Width,
      height: Height,
      fill: JSON.parse(Fill)
    });

    Element.parentElement.lastElementChild.style.transform = ' rotate('+Rotation+')';
  end;
end;

procedure TForm1.WebButton1Click(Sender: TObject);
begin
  Sparkline_Pie(
    PieChart1,                           // TWebHTMLDiv
    '867/5309',                          // Data
    '["red","green","blue"]',            // Fill
    '0deg'                               // Rotation
  );

  Sparkline_Pie(
    PieChart2,                           // TWebHTMLDiv
    '867,5309',                          // Data
    '["red","green","blue"]',            // Fill
    '90deg'                              // Rotation
  );

  Sparkline_Pie(
    PieChart3,                           // TWebHTMLDiv
    '8,6,7,5,3,0,9',                     // Data
    '["red","green","blue","yellow"]',   // Fill
    '45deg'                              // Rotation
  );

  Sparkline_Pie(
    PieChart4,                           // TWebHTMLDiv
    '8.6,7.5,3.09',                      // Data
    '["#888","#BBB","#EEE"]',            // Fill
    '180deg'                             // Rotation
  );

  Sparkline_Pie(
    PieChart5,                           // TWebHTMLDiv
    '8.67,53.09',                        // Data
    '["#888","#BBB","#EEE"]',            // Fill
    '-45deg'                             // Rotation
  );

  Sparkline_Pie(
    PieChart6,                           // TWebHTMLDiv
    '8.67,5,3.09',                       // Data
    '["#888","#800","#EEE"]',            // Fill
    '-90deg'                             // Rotation
  );

end;


The rotation is achieved by applying a CSS transform to the <span> that is created, which contains the generated SVG image. As it is a circle, rotating it does what we'd expect. It is also possible to manipulate the display of the SVG in other ways if it needs to be resized or shifted around, for example, using plain old CSS.  Here's what our pie charts look like.


TMS Software Delphi  Components
Sparkline "Pie" Examples.

And finally, donut charts are similar to pie charts, with an extra parameter available for the inner radius. It usually defaults to half the outer radius but we'll set it as a parameter to ensure that we can adjust as needed. We'll also add an extra parameter to display text in the donut, in case we want to use it as a progress indicator with a percentage, or something along those lines.

This could be wrapped in another function that does the math (for example, passing a percent number and setting the data to be "percent/100" sort of idea if this were to be used frequently in your project. Lots of options, as usual.


procedure TForm1.Sparkline_Donut(Chart: TWebHTMLDiv; ChartData: String; Fill: String; Rotation: String; InnerRadius: Double; DisplayText: String);
var
  Element: TJSElement;
  Width: Integer;
  Height: Integer;

begin
  // Create a place to attach the Sparkline
  Chart.ElementHandle.innerHTML := '<span></span>';

  // Add data to this place
  Element := Chart.ElementHandle.firstElementChild;
  Element.innerHTML := ChartData;

  // Get dimensions from size of encompassing DIV
  Width := Chart.Width;
  Height := Chart.Height;

  asm
    peity(Element, "pie", {
      width: Width,
      height: Height,
      fill: JSON.parse(Fill),
      innerRadius: InnerRadius
    });

    Element.parentElement.lastElementChild.style.transform = ' rotate('+Rotation+')';

    const newdiv = document.createElement("div");
    const newtxt = document.createTextNode(DisplayText);
    newdiv.appendChild(newtxt);
    newdiv.style.cssText = 'position:absolute; display:flex; align-items:center; justify-content:center; width:100%; height:100%; top:0px; left:0px; color:#fff; font-size:10px;';
    Element.parentElement.appendChild(newdiv);
  end;
end;

procedure TForm1.WebButton1Click(Sender: TObject);
begin
  Sparkline_Donut(
    DonutChart1,                         // TWebHTMLDiv
    '867/5309',                          // Data
    '["red","green","blue"]',            // Fill
    '0deg,',                             // Rotation
    20,                                  // Inner Radius
    ''                                   // Text
  );

  Sparkline_Donut(
    DonutChart2,                         // TWebHTMLDiv
    '867,5309',                          // Data
    '["red","green","blue"]',            // Fill
    '90deg',                             // Rotation
    10,                                  // Inner Radius
    ''                                   // Text
  );

  Sparkline_Donut(
    DonutChart3,                         // TWebHTMLDiv
    '8,6,7,5,3,0,9',                     // Data
    '["red","green","blue","yellow"]',   // Fill
    '45deg',                             // Rotation
    18,                                  // Inner Radius
    '2%'                                 // Text
  );

  Sparkline_Donut(
    DonutChart4,                         // TWebHTMLDiv
    '8.6,7.5,3.09',                      // Data
    '["#888","#BBB","#EEE"]',            // Fill
    '180deg',                            // Rotation
    18,                                  // Inner Radius
    'ABCD'                               // Text
  );

  Sparkline_Donut(
    DonutChart5,                         // TWebHTMLDiv
    '8.67,53.09',                        // Data
    '["#888","#BBB","#EEE"]',            // Fill
    '-45deg',                            // Rotation
    12,                                  // Inner Radius
    '41m'                               // Text
  );

  Sparkline_Donut(
    DonutChart6,                         // TWebHTMLDiv
    '8.67,5,3.09',                       // Data
    '["#888","#800","#EEE"]',            // Fill
    '-90deg',                            // Rotation
    15,                                  // Inner Radius
    '12%'                                // Text
  );
end;


It also shouldn't be any trouble at all to imagine how one might add additional parameters if the format of the text inside the donut needs to be customized on a per-donut basis, or if you were interested in having multiple lines of text or multiple colors, etc. The same applies to adding text outside of any of these charts - we're just doing everything ourselves here, which is bad in that it takes more effort on our part, but also good in that we don't have to fiddle with what someone else thinks might be a good font size or placement. Here's what we've got so far in terms of donut charts.


TMS Software Delphi  Components
Sparkline "Donut" Examples.


Moving Along.

So with that all sorted, we can now easily create line, bar, pie, and donut charts with a Delphi function call, so long as we have a TWebHTMLDiv component as a placeholder. This could be further refined by supporting other web controls as placeholders, other functions with fewer parameters (or more parameters!), or functions that do more of the work in terms of things like calculating percentages or colors or any of the other parameter elements that these charts use.  Most of that will be specific to what kinds of charts your particular project uses or the kinds of variability you want to have in the charts that are displayed. 

The generated SVG files are also transparent by default, so they could be overlaid, or their transparency adjusted to bypass the idea that there can only be one data series displayed.  For example, an inner donut and an outer donut might be used to display different bits of data. Or two line charts overlaid to show two series. Naturally the more complex the display, the more likely it is that it will need some additional data for reference. Keeping it simple is what Sparklines are all about, after all.

Another aspect of these kinds of charts is that they don't have to be static. A donut displaying a progress bar is one example of a "moving" chart. A bar chart that updates a stock value or a line chart that updates a heart rate might be other examples where a simple Sparkline can relay considerably more information. Using what we've got, along with a timer, we should be able to implement these kinds of things without too much effort.

For a progress bar,  we'll have the timer call "UpdateProgress" with a value, and it will then pass the necessary values to redraw the chart.


procedure TForm1.UpdateProgress(Progress, Tasks: Double);
begin
  Sparkline_Donut(
    ProgressChart1,
    FloatToStr(Progress)+'/'+FloatToStr(Tasks),
    '["#800","#FFF"]',          
    '0deg',                     
    18,                         
    FloatToStrF(100.0*Progress/Tasks,ffNumber,5,1)+'%'
  );
end;

procedure TForm1.WebTimer1Timer(Sender: TObject);
const
  tasks = 78;
begin
  WebTimer1.Tag := webTimer1.Tag + 1;

  UpdateProgress(WebTimer1.Tag, Tasks);

  if WebTimer1.Tag = Tasks
  then WebTimer1.Tag := 0;
end;


For a moving bar chart, let's use an example of just displaying a moving subset of the digits of Pi as a stand-in for actual data. Our timer will in effect just display the next subset, one digit over, each time it is updated. We'll make it a bit fancier by coloring every tenth bar a different color, so we should see a more interesting animation.


procedure TForm1.WebTimer2Timer(Sender: TObject);
var
  chartvalues: String;
  chartcolors: String;
  i:integer;
begin
  WebTimer2.Tag := WebTimer2.Tag + 1;

  chartvalues := PiString[WebTimer2.Tag];
  for i := 1 to 100 do
    chartvalues := chartvalues+','+PiString[(WebTimer2.Tag + i) mod 1000];

  chartcolors := '[';
  for i := 0 to 9 do
    if ((webTimer2.Tag + i) mod 10) = 0
    then chartcolors := chartcolors+'"#F0F",'
    else chartcolors := chartcolors+'"#FFF",';
  chartcolors := Copy(chartcolors,1,length(chartcolors)-1)+']';

  Sparkline_Bar(
    MovingBarChart1,
    chartvalues,
    '0',           
    '10',          
    chartcolors,
    0.01           
  );
end;


For a moving line chart, let's try to mimic a heartbeat. The hardest part is likely to find an ECG dataset that looks interesting. A tiny subset of this dataset is used here. The sample data in this case is loaded from a CSV file into a TStringList. Then, the trick employed here is to just cycle through a subset of this data. To make it a little more interesting, another <div> element with an opacity gradient is overlaid on top of the chart to mimic the data fading out. Not necessarily an efficient way to go about it, but it gets us the desired effect.


procedure TForm1.LoadHeartData;
begin
  HeartData := TStringList.Create;
  HeartData.LoadFromFile('heartdata.csv');
end;

procedure TForm1.WebTimer3Timer(Sender: TObject);
var
  DataSubset: TStringList;
  i: integer;

begin
  WebTimer3.Tag := WebTimer3.Tag + 5;

  DataSubset := TSTringList.Create;
  for i := 1 to 400 do
    DataSubset.Add(HeartData[(WebTimer3.Tag + i) mod HeartData.Count]);

  Sparkline_Line(
    ECGChart1,
    DataSubset.CommaText,
    '-3',
    '3',
    'transparent',
    'white',
    2
  );

  DataSubset.Free;
end;


The end result is that we've got some passable animations for our Sparkline charts.  Here's what they look like.


TMS Software Delphi  Components
Animated Sparklines.


Next Steps.

That about covers Sparklines, at least for now. Any kind of implementation beyond this, involving interactivity or more complex displays of data, is likely moving well beyond what Sparklines are intended to do. There are other charting libraries better suited to those sorts of things, and we'll be looking at them in some of the upcoming posts. The attached project file contains everything we've covered here, including the sample ECG data used in the above animation.

Download sample


Related Posts

Charting Part 1: Sparklines
Charting Part 2: Chart.js
Charting Part 3: D3.js Introduction
Charting Part 4: D3.js Interaction
Charting Part 5: D3.js Animation
Charting Part 6: Tag Clouds

Follow Andrew on 𝕏 at @WebCoreAndMore or join our
𝕏
Web Core and More Community.



Andrew Simard




This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post