Add dfl.chart.TimeChartRenderer with example code

This commit is contained in:
haru-s 2024-04-13 15:40:56 +09:00
parent ccca379024
commit 2f84718bd3
11 changed files with 683 additions and 3 deletions

View file

@ -6,8 +6,8 @@ DFL is a Win32 windowing library for the D language.
## Recent major features
- **Module "dfl.chart" is now comming.**
- TableRenderer (with example)
- **LineGraphRenderer (with example)**
- ~TimeChartRenderer~
- LineGraphRenderer (with example)
- **TimeChartRenderer (with example)**
- Add simple clock "Dclock" as an example of DFL application.
- Module "dfl.printing" is now comming.
- PrintDialog
@ -41,6 +41,7 @@ DFL is a Win32 windowing library for the D language.
![screen shot](./examples/dclock/image/screenshot.png "screen shot")
![screen shot](./examples/tablerenderer/image/screenshot.png "screen shot")
![screen shot](./examples/linegraphrenderer/image/screenshot.png "screen shot")
![screen shot:w200](./examples/timechartrenderer/image/screenshot.png "screen shot")
## Build and Install (dfl.lib and dfl_debug.lib)
### 1. Set environment variables

16
examples/timechartrenderer/.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
.dub
docs.json
__dummy.html
docs/
/hello_dfl
hello_dfl.so
hello_dfl.dylib
hello_dfl.dll
hello_dfl.a
hello_dfl.lib
hello_dfl-test-*
*.exe
*.pdb
*.o
*.obj
*.lst

View file

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "C++ Launch (Windows) timechartrenderer",
"type": "cppvsdbg",
"request": "launch",
"cwd": "${workspaceRoot}",
"program": "./bin/timechartrenderer.exe",
"console": "internalConsole"
}
]
}

View file

@ -0,0 +1,20 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "dub",
"run": false,
"cwd": ".",
"compiler": "$current",
"archType": "$current",
"buildType": "$current",
"configuration": "$current",
"problemMatcher": [
"$dmd"
],
"group": "build",
"label": "dub: Build timechartrenderer_sample",
"detail": "dub build --compiler=dmd.EXE -a=x86_64 -b=debug -c=application"
}
]
}

View file

@ -0,0 +1,2 @@
# Screen Shot
![screen shot](./image/screenshot.png "screen shot")

View file

@ -0,0 +1,16 @@
{
"authors": ["haru-s"],
"copyright": "Copyright (C) 2024 haru-s",
"description": "DFL sample code.",
"name": "timechartrenderer",
"targetType": "executable",
"targetPath": "bin",
"dependencies": {
"dfl": {
"path": "../../../dfl"
}
},
"lflags-windows-x86_omf-dmd": ["/exet:nt/su:windows:6.0"],
"lflags-windows-x86_mscoff-dmd": ["/SUBSYSTEM:WINDOWS", "/ENTRY:mainCRTStartup"],
"lflags-windows-x86_64-dmd": ["/SUBSYSTEM:WINDOWS", "/ENTRY:mainCRTStartup"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,3 @@
set dmd_path=c:\d\dmd2\windows
set dmc_path=c:\dmc\dm
cmd

View file

@ -0,0 +1,89 @@
import dfl;
version(Have_dfl) // For DUB.
{
}
else
{
pragma(lib, "dfl.lib");
}
class MainForm : Form
{
alias CustomTimeChartRenderer = TimeChartRenderer!(int,int,int,int,int,int,int,int,int);
CustomTimeChartRenderer _graph;
alias CustomTableRenderer = TableRenderer!(int,int,int,int,int,int,int,int,int);
CustomTableRenderer _table;
public this()
{
this.text = "TimeChartRenderer example";
this.size = Size(600, 650);
string csv =
"Time (ms),D1,D2,D3,D4,A1,A2,A3,A4\n" ~
"0,0,0,0,0,0,0,0,0\n" ~
"100,1,0,0,0,5,2,10,2\n" ~
"200,0,1,0,0,6,3,10,-3\n" ~
"300,1,1,1,0,7,4,9,4\n" ~
"400,0,0,1,1,8,5,9,-5\n" ~
"500,1,0,1,1,9,2,8,6\n" ~
"600,0,0,0,1,8,3,8,-7\n" ~
"700,1,1,0,1,7,4,7,8\n" ~
"800,0,1,0,0,6,5,7,-9\n" ~
"900,1,0,1,0,5,2,6,10\n" ~
"1000,0,0,1,0,4,3,6,-10\n" ~
"1100,1,1,1,0,3,4,5,9\n" ~
"1200,0,1,0,1,2,5,5,-9\n" ~
"1300,1,0,0,1,1,2,4,8\n" ~
"1400,0,0,0,1,0,3,4,-8\n";
_graph = new CustomTimeChartRenderer(csv, 15);
_graph.location = Point(50, 50);
_graph.chartMargins = ChartMargins(50, 50, 50, 50);
_graph.seriesStyleList[0..4] = TimeChartSeriesStyle(true, Color.blue, 20); // Digital
_graph.seriesStyleList[4..7] = TimeChartSeriesStyle(false, Color.red, 50, 0, 10); // Analog
_graph.seriesStyleList[7] = TimeChartSeriesStyle(false, Color.red, 100, -10, 20,); // Analog
_graph.plotAreaTopPadding = 20;
_graph.plotAreaBottomPadding = 20;
_graph.plotAreaLeftPadding = 20;
_graph.plotAreaRightPadding = 20;
_graph.plotAreaBoundsColor = Color.black;
_graph.plotAreaAndHorizontalScaleSpanY = 10;
_graph.hasHorizontalScale = true;
_graph.horizontalScaleSpan = 20;
_graph.horizontalScaleStep = 2;
_graph.horizontalScaleLineInnerSide = 5;
_graph.horizontalScaleLineOuterSide = 5;
_graph.horizontalScaleHeight = 20;
_graph.hasVerticalScale = true;
_graph.verticalScaleWidth = 40;
_graph.hasZeroLine = true;
_graph.backColor = Color.white;
_table = new CustomTableRenderer(csv, 10);
_table.location = Point(50, 500);
_table.hasHeader = true;
_table.showHeader = true;
_table.headerLine = true;
_table.width[] = 50;
}
protected override void onPaint(PaintEventArgs e)
{
if (_graph)
_graph.draw(e.graphics);
// if (_table)
// _table.draw(e.graphics);
}
}
static this()
{
Application.enableVisualStyles();
}
void main()
{
Application.run(new MainForm());
}

View file

@ -0,0 +1,12 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"d.projectImportPaths": [
"..\\..\\..\\dfl\\source"
]
}
}

View file

@ -960,4 +960,512 @@ enum PlotPointForm
RECTANGLE,
CROSS,
TRIANGLE,
}
}
///
class TimeChartRenderer(T...)
{
///
this(string csv, int numRecords)
{
this(csv);
_firstRecord = 0;
_lastRecord = numRecords - 1;
}
/// ditto
this(string csv, int first, int last)
{
this(csv);
_firstRecord = first;
_lastRecord = last;
}
/// ditto
this(string csv) // deprecated
{
_csv = csv;
_chartMargins = ChartMargins(50, 50, 50, 50);
_backColor = Color.white;
_plotAreaBoundsColor = Color.black;
_plotAreaTopPadding = 20;
_plotAreaBottomPadding = 20;
_plotAreaLeftPadding = 20;
_plotAreaRightPadding = 20;
_plotAreaAndHorizontalScaleSpanY = 10;
_hasVerticalScale = true;
_verticalScaleWidth = 40;
_hasHorizontalScale = true;
_horizontalScaleSpan = 50;
_horizontalScaleStep = 1;
_horizontalScaleLineInnerSide = 5;
_horizontalScaleLineOuterSide = 5;
_horizontalScaleHeight = 25;
_hasZeroLine = true;
for (int i; i < cast(int)T.length - 1; i++)
_seriesStyleList ~= TimeChartSeriesStyle(false);
}
/// Draw records.
void draw(Graphics g)
{
string hSubject = csvReader!(Tuple!T)(_csv, null).header.front;
enum HORIZONTAL_SCALE_AREA_AND_SUBJECT_SPAN_Y = 25; // TODO: fix.
enum HORIZONTAL_SCALE_TEXT_HEIGHT = 12; // TODO: fix.
// Draw background.
Rect backgroundRect = Rect(
plotAreaBounds.x - _chartMargins.left,
plotAreaBounds.y - _chartMargins.top,
plotAreaBounds.width + _chartMargins.left + _chartMargins.right,
plotAreaBounds.height + _chartMargins.top + _chartMargins.bottom
);
if (_hasVerticalScale)
{
backgroundRect.x = plotAreaBounds.x - _chartMargins.left - _verticalScaleWidth,
backgroundRect.width = plotAreaBounds.width + _chartMargins.left + _chartMargins.right + _verticalScaleWidth;
}
if (_hasHorizontalScale)
{
backgroundRect.height += _horizontalScaleHeight + _plotAreaAndHorizontalScaleSpanY;
if (hSubject != "")
backgroundRect.height += HORIZONTAL_SCALE_AREA_AND_SUBJECT_SPAN_Y + HORIZONTAL_SCALE_TEXT_HEIGHT;
}
g.fillRectangle(new SolidBrush(_backColor), backgroundRect);
// Draw bounds of plot area.
g.drawRectangle(new Pen(_plotAreaBoundsColor), plotAreaBounds);
// Draw vertical scale.
if (_hasVerticalScale)
{
enum LINE_HEIGHT = 12f;
Font f = new Font("MS Gothic", LINE_HEIGHT);
int x = plotAreaBounds.x - _verticalScaleWidth;
int seriesIndex;
foreach (seriesName; csvReader!(Tuple!T)(_csv, null).header.dropOne)
{
// Draw vertical scalse label.
enum VERTICAL_SCALE_LABEL_TWEAK = 5;
int y = _seriesBaseY(seriesIndex) - cast(int)LINE_HEIGHT - VERTICAL_SCALE_LABEL_TWEAK;
int currHeight = _seriesStyleList[cast(int)seriesIndex].height;
g.drawText(seriesName, f, Color.black, Rect(x, y, 100, 100));
// Draw analog scales.
if (!_seriesStyleList[seriesIndex].isDigital)
{
enum TWEAK_X = 2;
int maxValue = _seriesStyleList[cast(int)seriesIndex].max;
string maxText = to!string(maxValue);
g.drawText(
maxText,
f,
Color.black,
Rect(plotAreaBounds.x + TWEAK_X, _seriesBaseY(seriesIndex) - currHeight, 100, 100)
);
int minValue = _seriesStyleList[cast(int)seriesIndex].min;
string minText = to!string(minValue);
g.drawText(
minText,
f,
Color.black,
Rect(plotAreaBounds.x + TWEAK_X, y, 100, 100)
);
// Draw zero line and scale label.
if (_hasZeroLine && minValue < 0)
{
g.drawLine(
new Pen(Color.lightGray),
plotAreaBounds.x,
_seriesZeroY(seriesIndex),
plotAreaBounds.right - 1,
_seriesZeroY(seriesIndex),
);
g.drawText(
"0",
f,
Color.black,
Rect(plotAreaBounds.x + TWEAK_X, _seriesZeroY(seriesIndex) - cast(int)LINE_HEIGHT - VERTICAL_SCALE_LABEL_TWEAK, 100, 100)
);
}
}
// Draw vertical scalse line.
g.drawLine(
new Pen(Color.lightGray),
x,
_seriesBaseY(seriesIndex),
x + _verticalScaleWidth,
_seriesBaseY(seriesIndex)
);
// Draw vertical zero line in plot area.
if (_hasZeroLine)
{
g.drawLine(
new Pen(Color.lightGray),
x + _verticalScaleWidth,
_seriesBaseY(seriesIndex),
plotAreaBounds.right - 1,
_seriesBaseY(seriesIndex)
);
}
seriesIndex++;
}
}
// Draw horizontal scale.
if (_hasHorizontalScale)
{
// Draw horizontal scale label.
int index;
foreach (record; csvReader!(Tuple!T)(_csv, null).drop(_firstRecord).take(_lastRecord - _firstRecord + 1).stride(_horizontalScaleStep))
{
int x = plotAreaBounds.x + _plotAreaLeftPadding + index * _horizontalScaleSpan;
int y = plotAreaBounds.bottom + _plotAreaAndHorizontalScaleSpanY;
g.drawText(
to!string(record[0]),
new Font("MS Gothic", 12f),
_plotAreaBoundsColor,
Rect(x, y, _horizontalScaleSpan * _horizontalScaleStep, _horizontalScaleHeight)
);
index += _horizontalScaleStep;
}
// Draw horizontal scale line.
for (int i; i < _lastRecord - _firstRecord + 1; i += _horizontalScaleStep)
{
int x = plotAreaBounds.x + _plotAreaLeftPadding + i * _horizontalScaleSpan;
int y = plotAreaBounds.bottom;
g.drawLine(
new Pen(_plotAreaBoundsColor),
x,
y - _horizontalScaleLineInnerSide,
x,
y + _horizontalScaleLineOuterSide
);
}
// Draw horizontal scale subject.
{
int x = plotAreaBounds.x;
int y = plotAreaBounds.bottom + _horizontalScaleHeight + HORIZONTAL_SCALE_AREA_AND_SUBJECT_SPAN_Y;
auto fmt = new TextFormat;
fmt.alignment = TextAlignment.CENTER;
g.drawText(
hSubject,
new Font("MS Gothic", 12f),
_plotAreaBoundsColor,
Rect(x, y, plotAreaBounds.width, HORIZONTAL_SCALE_TEXT_HEIGHT * 2),
fmt
);
}
}
// Draw records.
Tuple!T prevRecord;
int x1 = plotAreaBounds.x + _plotAreaLeftPadding;
int x2;
bool isFirstRecord = true;
auto csvRange = csvReader!(Tuple!T)(_csv, null).drop(_firstRecord).take(_lastRecord - _firstRecord + 1);
foreach (currRecord; csvRange)
{
x2 = x1 + _horizontalScaleSpan;
bool isFirstColumn = true;
foreach (col, value; currRecord)
{
if (col != 0)
{
int seriesIndex = cast(int)col - 1;
if (!isFirstRecord)
{
bool isDigital = _seriesStyleList[seriesIndex].isDigital;
int currHeight = _seriesStyleList[seriesIndex].height;
Color lineColor = _seriesStyleList[seriesIndex].color;
if (isDigital)
{ // Digital signal
enum MODEST_HEIGHT_RATIO = 0.8;
int toDigit(int v) { return v == 0 ? 0 : 1; }
int y1 = cast(int)(_seriesBaseY(seriesIndex) - currHeight * toDigit(prevRecord[col]) * MODEST_HEIGHT_RATIO);
int y2 = cast(int)(_seriesBaseY(seriesIndex) - currHeight * toDigit(value) * MODEST_HEIGHT_RATIO);
g.drawLine(
new Pen(lineColor),
x1 - _horizontalScaleSpan,
y1,
x2 - _horizontalScaleSpan,
y1
);
g.drawLine(
new Pen(lineColor),
x2 - _horizontalScaleSpan,
y1,
x2 - _horizontalScaleSpan,
y2
);
}
else
{ // Analog signal
double vRatio = cast(double)currHeight / (_seriesStyleList[seriesIndex].max - _seriesStyleList[seriesIndex].min);
int y1 = cast(int)(_seriesBaseY(seriesIndex) - vRatio * prevRecord[col]);
int y2 = cast(int)(_seriesBaseY(seriesIndex) - vRatio * value);
// Offset the base line of zero point.
if (_seriesStyleList[seriesIndex].min < 0)
{
y1 -= _seriesBaseY(seriesIndex) - _seriesZeroY(seriesIndex);
y2 -= _seriesBaseY(seriesIndex) - _seriesZeroY(seriesIndex);
}
g.drawLine(
new Pen(lineColor),
x1 - _horizontalScaleSpan,
y1,
x2 - _horizontalScaleSpan,
y2
);
}
}
}
isFirstColumn = false;
}
x1 = x1 + _horizontalScaleSpan;
prevRecord = currRecord;
isFirstRecord = false;
}
}
///
Rect plotAreaBounds() const
{
return Rect(
_originPoint.x,
_originPoint.y,
cast(int)((_lastRecord - _firstRecord) * _horizontalScaleSpan + _plotAreaLeftPadding + _plotAreaRightPadding),
_plotAreaTopPadding + _verticalSeriesOffset(cast(int)T.length - 2) + _plotAreaBottomPadding
);
}
///
deprecated void originPoint(Point pt)
{
_originPoint = pt;
}
///
void location(Point pt)
{
_originPoint.x = pt.x + _chartMargins.top + _verticalScaleWidth;
_originPoint.y = pt.y + _chartMargins.left;
}
///
void chartMargins(ChartMargins m)
{
_chartMargins = m;
}
///
void plotAreaAndHorizontalScaleSpanY(int y)
{
_plotAreaAndHorizontalScaleSpanY = y;
}
///
void plotAreaTopPadding(int y)
{
_plotAreaTopPadding = y;
}
///
void plotAreaBottomPadding(int y)
{
_plotAreaBottomPadding = y;
}
///
void plotAreaRightPadding(int x)
{
_plotAreaRightPadding = x;
}
///
void plotAreaLeftPadding(int x)
{
_plotAreaLeftPadding = x;
}
///
void hasHorizontalScale(bool byes)
{
_hasHorizontalScale = byes;
}
///
void hasVerticalScale(bool byes)
{
_hasVerticalScale = byes;
}
///
void hasZeroLine(bool byes)
{
_hasZeroLine = byes;
}
///
void firstRecord(int i)
{
_firstRecord = i;
}
///
void lastRecord(int i)
{
_lastRecord = i;
}
///
void backColor(Color c)
{
_backColor = c;
}
///
void plotAreaBoundsColor(Color c)
{
_plotAreaBoundsColor = c;
}
///
void horizontalScaleSpan(int x)
{
_horizontalScaleSpan = x;
}
///
void horizontalScaleStep(int s)
{
if (s <= 0)
throw new DflException("DFL: Invalid horizontal scale step.");
_horizontalScaleStep = s;
}
///
void verticalScaleWidth(int w)
{
_verticalScaleWidth = w;
}
///
void horizontalScaleLineInnerSide(int h)
{
_horizontalScaleLineInnerSide = h;
}
///
void horizontalScaleLineOuterSide(int h)
{
_horizontalScaleLineOuterSide = h;
}
///
void horizontalScaleHeight(int h)
{
_horizontalScaleHeight = h;
}
///
struct TimeChartSeriesStyleObject // Internal struct.
{
///
this(TimeChartSeriesStyle[] v)
{
_arr = v;
}
/// Assign operator forwarding.
void opIndexAssign(TimeChartSeriesStyle value)
{
_arr[] = value;
}
/// ditto
void opIndexAssign(TimeChartSeriesStyle value, size_t i)
{
_arr[i] = value;
}
/// ditto
void opSliceAssign(TimeChartSeriesStyle value, size_t i, size_t j)
{
_arr[i..j] = value;
}
///
TimeChartSeriesStyle opIndex(size_t i)
{
return _arr[i];
}
private:
TimeChartSeriesStyle[] _arr;
}
///
TimeChartSeriesStyleObject seriesStyleList()
{
return TimeChartSeriesStyleObject(_seriesStyleList);
}
private:
string _csv;
int _firstRecord;
int _lastRecord;
Point _originPoint;
ChartMargins _chartMargins;
Color _backColor;
Color _plotAreaBoundsColor;
int _plotAreaTopPadding;
int _plotAreaBottomPadding;
int _plotAreaLeftPadding;
int _plotAreaRightPadding;
int _plotAreaAndHorizontalScaleSpanY;
bool _hasHorizontalScale;
int _horizontalScaleSpan;
int _horizontalScaleStep;
int _horizontalScaleHeight;
int _horizontalScaleLineInnerSide;
int _horizontalScaleLineOuterSide;
bool _hasVerticalScale;
int _verticalScaleWidth;
bool _hasZeroLine;
TimeChartSeriesStyle[] _seriesStyleList;
///
int _verticalSeriesOffset(int seriesIndex) const
{
int y;
for (int i; i <= seriesIndex; i++)
y += _seriesStyleList[i].height;
return y;
}
///
int _seriesBaseY(int seriesIndex) const
{
return plotAreaBounds.y + _plotAreaTopPadding + _verticalSeriesOffset(seriesIndex);
}
///
int _seriesZeroY(int seriesIndex)
{
int currHeight = _seriesStyleList[cast(int)seriesIndex].height;
int maxValue = _seriesStyleList[cast(int)seriesIndex].max;
int minValue = _seriesStyleList[cast(int)seriesIndex].min;
return cast(int)(_seriesBaseY(seriesIndex) + cast(double)minValue * currHeight / (maxValue - minValue));
}
}
///
struct TimeChartSeriesStyle
{
this(bool inIsDigital, Color inColor = Color.blue, int inHeight = 20, int inMin = 0, int inMax = 1)
{
isDigital = inIsDigital;
color = inColor;
height = inHeight;
min = inMin;
max = inMax;
}
bool isDigital;
Color color;
int height;
int min;
int max;
}