Skip to content

Commit

Permalink
Report helper is added.
Browse files Browse the repository at this point in the history
  • Loading branch information
cinar authored Dec 8, 2023
1 parent a631675 commit b658e2b
Show file tree
Hide file tree
Showing 5 changed files with 593 additions and 0 deletions.
83 changes: 83 additions & 0 deletions helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ The information provided on this project is strictly for informational purposes
- [type Float](<#Float>)
- [type Integer](<#Integer>)
- [type Number](<#Number>)
- [type Report](<#Report>)
- [func NewReport\(title string, date \<\-chan time.Time\) \*Report](<#NewReport>)
- [func \(r \*Report\) AddColumn\(column ReportColumn\)](<#Report.AddColumn>)
- [func \(r \*Report\) WriteToFile\(fileName string\) error](<#Report.WriteToFile>)
- [func \(r \*Report\) WriteToWriter\(writer io.Writer\) error](<#Report.WriteToWriter>)
- [type ReportColumn](<#ReportColumn>)
- [func NewNumericReportColumn\[T Number\]\(name string, values \<\-chan T\) ReportColumn](<#NewNumericReportColumn>)
- [type Ring](<#Ring>)
- [func NewRing\[T any\]\(size int\) \*Ring\[T\]](<#NewRing>)
- [func \(r \*Ring\[T\]\) Insert\(t T\) T](<#Ring[T].Insert>)
Expand Down Expand Up @@ -831,6 +838,82 @@ type Number interface {
}
```

<a name="Report"></a>
## type [Report](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L80-L82>)

Report generates an HTML file containing an interactive chart that visually represents the provided data and annotations.

The generated HTML file can be opened in a web browser to explore the data visually, interact with the chart elements, and view the associated annotations.

```go
type Report struct {
// contains filtered or unexported fields
}
```

<a name="NewReport"></a>
### func [NewReport](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L87>)

```go
func NewReport(title string, date <-chan time.Time) *Report
```

NewReport takes a channel of time as the time axis and returns a new instance of the Report struct. This instance can later be used to add data and annotations and subsequently generate a report.

<a name="Report.AddColumn"></a>
### func \(\*Report\) [AddColumn](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L98>)

```go
func (r *Report) AddColumn(column ReportColumn)
```

AddColumn adds a new data column to the report.

<a name="Report.WriteToFile"></a>
### func \(\*Report\) [WriteToFile](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L117>)

```go
func (r *Report) WriteToFile(fileName string) error
```

WriteToFile writes the generated report content to a file with the specified name. This allows users to conveniently save the report for later viewing or analysis.

<a name="Report.WriteToWriter"></a>
### func \(\*Report\) [WriteToWriter](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L105>)

```go
func (r *Report) WriteToWriter(writer io.Writer) error
```

WriteToWriter writes the report content to the provided io.Writer. This allows the report to be sent to various destinations, such as a file, a network socket, or even the standard output.

<a name="ReportColumn"></a>
## type [ReportColumn](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L24-L33>)

ReportColumn defines the interface that all report data columns must implement. This interface ensures that different types of data columns can be used consistently within the report generation process.

```go
type ReportColumn interface {
// Name returns the name of the report column.
Name() string

// Type returns the data type of the report column.
Type() string

// Value returns the next data value for the report column.
Value() string
}
```

<a name="NewNumericReportColumn"></a>
### func [NewNumericReportColumn](<https://github.com/cinar/indicator/blob/v2/helper/report.go#L43>)

```go
func NewNumericReportColumn[T Number](name string, values <-chan T) ReportColumn
```

NewNumericReportColumn returns a new instance of a numeric data column for a report.

<a name="Ring"></a>
## type [Ring](<https://github.com/cinar/indicator/blob/v2/helper/ring.go#L18-L21>)

Expand Down
126 changes: 126 additions & 0 deletions helper/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright (c) 2023 Onur Cinar. All Rights Reserved.
// The source code is provided under MIT License.
// https://github.com/cinar/indicator

package helper

import (
// Go embed report template.
_ "embed"
"fmt"
"io"
"os"
"path/filepath"
"text/template"
"time"
)

//go:embed "report.tmpl"
var reportTmpl string

// ReportColumn defines the interface that all report data columns must implement.
// This interface ensures that different types of data columns can be used
// consistently within the report generation process.
type ReportColumn interface {
// Name returns the name of the report column.
Name() string

// Type returns the data type of the report column.
Type() string

// Value returns the next data value for the report column.
Value() string
}

// numericReportColumn is the number report column struct.
type numericReportColumn[T Number] struct {
ReportColumn
name string
values <-chan T
}

// NewNumericReportColumn returns a new instance of a numeric data column for a report.
func NewNumericReportColumn[T Number](name string, values <-chan T) ReportColumn {
return &numericReportColumn[T]{
name: name,
values: values,
}
}

// Name returns the name of the report column.
func (c *numericReportColumn[T]) Name() string {
return c.name
}

// Type returns number as the data type.
func (c *numericReportColumn[T]) Type() string {
return "number"
}

// Value returns the next data value for the report column.
func (c *numericReportColumn[T]) Value() string {
return fmt.Sprintf("%v", <-c.values)
}

// reportModel struct holds the data that is exposed to the template renderer
// for generating the report. It encapsulates all the information necessary
// to render the report's content, including data, and annotations.
type reportModel struct {
Title string
Date <-chan time.Time
Columns []ReportColumn
}

// Report generates an HTML file containing an interactive chart that
// visually represents the provided data and annotations.
//
// The generated HTML file can be opened in a web browser to explore
// the data visually, interact with the chart elements, and view
// the associated annotations.
type Report struct {
model reportModel
}

// NewReport takes a channel of time as the time axis and returns a new
// instance of the Report struct. This instance can later be used to
// add data and annotations and subsequently generate a report.
func NewReport(title string, date <-chan time.Time) *Report {
return &Report{
model: reportModel{
Title: title,
Date: date,
Columns: []ReportColumn{},
},
}
}

// AddColumn adds a new data column to the report.
func (r *Report) AddColumn(column ReportColumn) {
r.model.Columns = append(r.model.Columns, column)
}

// WriteToWriter writes the report content to the provided io.Writer.
// This allows the report to be sent to various destinations, such
// as a file, a network socket, or even the standard output.
func (r *Report) WriteToWriter(writer io.Writer) error {
tmpl, err := template.New("report").Parse(reportTmpl)
if err != nil {
return err
}

return tmpl.Execute(writer, r.model)
}

// WriteToFile writes the generated report content to a file with
// the specified name. This allows users to conveniently save the
// report for later viewing or analysis.
func (r *Report) WriteToFile(fileName string) error {
file, err := os.Create(filepath.Clean(fileName))
if err != nil {
return err
}

defer file.Close()

return r.WriteToWriter(file)
}
86 changes: 86 additions & 0 deletions helper/report.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
</head>

<body>
<section class="section">
<div class="container">
<h1 class="title">
{{ .Title }}
</h1>

<div id="dashboard">
<div id="chart"></div>
<div id="controls"></div>
</div>
</div>
</section>

<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
// Load the Visualization API and the corechart and controls packages.
google.charts.load("current", { "packages": ["corechart", "controls"] });

// Set a callback to run when the Google Visualization API is loaded.
google.charts.setOnLoadCallback(drawDashboard);

// Callback that creates and populates a data table,
// instantiates the pie chart, passes in the data and
// draws it.
function drawDashboard() {
var dashboard = new google.visualization.Dashboard(document.getElementById("dashboard"));

var chart = new google.visualization.ChartWrapper({
"chartType": "LineChart",
"containerId": "chart",
"options": {
"title": "{{ .Title }}",
"curveType": "function",
"legend": {
"position": "right",
}
}
});

var rangeFilter = new google.visualization.ControlWrapper({
"controlType": "ChartRangeFilter",
"containerId": "controls",
"options": {
"filterColumnLabel": "Date",
"ui": {
"chartOptions": {
"height": 50
}
}
}
});

// Create the data table.
var data = new google.visualization.DataTable();
data.addColumn("date", "Date");
{{ range.Columns }}
data.addColumn("{{ .Type }}", "{{ .Name }}");
{{ end }}

{{ range.Date }}
data.addRow([
new Date({{ .Format "2006, 1, 2" }}),
{{ range $.Columns }}
{{ .Value }},
{{ end }}
]);
{{ end }}

dashboard.bind(rangeFilter, chart);
dashboard.draw(data);
}
</script>
</body>

</html>
46 changes: 46 additions & 0 deletions helper/report_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) 2023 Onur Cinar. All Rights Reserved.
// The source code is provided under MIT License.
// https://github.com/cinar/indicator

package helper_test

import (
"os"
"testing"
"time"

"github.com/cinar/indicator/helper"
)

func TestReportWriteToFile(t *testing.T) {
type Row struct {
Date time.Time `format:"2006-01-02"`
High float64
Low float64
Close float64
}

input, err := helper.ReadFromCsvFile[Row]("testdata/report.csv", true)
if err != nil {
t.Fatal(err)
}

inputs := helper.Duplicate(input, 4)
dates := helper.Map(inputs[0], func(row *Row) time.Time { return row.Date })
highs := helper.Map(inputs[1], func(row *Row) float64 { return row.High })
lows := helper.Map(inputs[2], func(row *Row) float64 { return row.Low })
closes := helper.Map(inputs[3], func(row *Row) float64 { return row.Close })

report := helper.NewReport("Test Report", dates)
report.AddColumn(helper.NewNumericReportColumn("High", highs))
report.AddColumn(helper.NewNumericReportColumn("Low", lows))
report.AddColumn(helper.NewNumericReportColumn("Close", closes))

fileName := "report.html"
defer os.Remove(fileName)

err = report.WriteToFile("report.html")
if err != nil {
t.Fatal(err)
}
}
Loading

0 comments on commit b658e2b

Please sign in to comment.