ortoni-report 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ortoni-report.d.ts +1 -0
- package/dist/ortoni-report.js +10 -10
- package/dist/ortoni-report.mjs +10 -10
- package/package.json +1 -1
- package/readme.md +77 -0
- package/report-template.hbs +251 -0
package/dist/ortoni-report.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ declare class OrtoniReport implements Reporter {
|
|
|
4
4
|
private results;
|
|
5
5
|
private groupedResults;
|
|
6
6
|
private suiteName;
|
|
7
|
+
constructor();
|
|
7
8
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
8
9
|
onTestBegin(test: TestCase, result: TestResult): void;
|
|
9
10
|
onTestEnd(test: TestCase, result: TestResult): void;
|
package/dist/ortoni-report.js
CHANGED
|
@@ -30,7 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/ortoni-report.ts
|
|
31
31
|
var ortoni_report_exports = {};
|
|
32
32
|
__export(ortoni_report_exports, {
|
|
33
|
-
default: () =>
|
|
33
|
+
default: () => ortoni_report_default
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(ortoni_report_exports);
|
|
36
36
|
var import_fs = __toESM(require("fs"));
|
|
@@ -38,12 +38,12 @@ var import_path = __toESM(require("path"));
|
|
|
38
38
|
var import_handlebars = __toESM(require("handlebars"));
|
|
39
39
|
var import_safe = __toESM(require("colors/safe"));
|
|
40
40
|
var OrtoniReport = class {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
constructor() {
|
|
42
|
+
this.results = [];
|
|
43
|
+
}
|
|
44
44
|
onBegin(config, suite) {
|
|
45
45
|
this.results = [];
|
|
46
|
-
const screenshotsDir = import_path.default.join(
|
|
46
|
+
const screenshotsDir = import_path.default.join(process.cwd(), "screenshots");
|
|
47
47
|
if (!import_fs.default.existsSync(screenshotsDir)) {
|
|
48
48
|
import_fs.default.mkdirSync(screenshotsDir);
|
|
49
49
|
}
|
|
@@ -51,7 +51,6 @@ var OrtoniReport = class {
|
|
|
51
51
|
onTestBegin(test, result) {
|
|
52
52
|
}
|
|
53
53
|
onTestEnd(test, result) {
|
|
54
|
-
console.log(test.titlePath());
|
|
55
54
|
const testResult = {
|
|
56
55
|
projectName: test.titlePath()[1],
|
|
57
56
|
// Get the project name
|
|
@@ -72,7 +71,7 @@ var OrtoniReport = class {
|
|
|
72
71
|
filePath: test.titlePath()[2]
|
|
73
72
|
};
|
|
74
73
|
if (result.attachments) {
|
|
75
|
-
const screenshotsDir = import_path.default.join(
|
|
74
|
+
const screenshotsDir = import_path.default.join(process.cwd(), "screenshots\\" + test.id);
|
|
76
75
|
if (!import_fs.default.existsSync(screenshotsDir)) {
|
|
77
76
|
import_fs.default.mkdirSync(screenshotsDir);
|
|
78
77
|
}
|
|
@@ -80,7 +79,7 @@ var OrtoniReport = class {
|
|
|
80
79
|
if (screenshot && screenshot.path) {
|
|
81
80
|
const screenshotContent = import_fs.default.readFileSync(screenshot.path, "base64");
|
|
82
81
|
const screenshotFileName = `screenshots/${test.id}/${import_path.default.basename(screenshot.path)}`;
|
|
83
|
-
import_fs.default.writeFileSync(import_path.default.join(
|
|
82
|
+
import_fs.default.writeFileSync(import_path.default.join(process.cwd(), screenshotFileName), screenshotContent, "base64");
|
|
84
83
|
testResult.screenshotPath = screenshotFileName;
|
|
85
84
|
}
|
|
86
85
|
}
|
|
@@ -110,12 +109,12 @@ var OrtoniReport = class {
|
|
|
110
109
|
return suiteName.split(" - ");
|
|
111
110
|
});
|
|
112
111
|
const html = this.generateHTML();
|
|
113
|
-
const outputPath = import_path.default.join(
|
|
112
|
+
const outputPath = import_path.default.join(process.cwd(), "ortoni-report.html");
|
|
114
113
|
import_fs.default.writeFileSync(outputPath, html);
|
|
115
114
|
console.log(`Ortoni HTML report generated at ${outputPath}`);
|
|
116
115
|
}
|
|
117
116
|
generateHTML() {
|
|
118
|
-
const templateSource = import_fs.default.readFileSync(import_path.default.join(
|
|
117
|
+
const templateSource = import_fs.default.readFileSync(import_path.default.join(process.cwd(), "report-template.hbs"), "utf-8");
|
|
119
118
|
const template = import_handlebars.default.compile(templateSource);
|
|
120
119
|
const data = {
|
|
121
120
|
suiteName: this.suiteName,
|
|
@@ -144,3 +143,4 @@ function safeStringify(obj, indent = 2) {
|
|
|
144
143
|
cache.clear();
|
|
145
144
|
return json;
|
|
146
145
|
}
|
|
146
|
+
var ortoni_report_default = OrtoniReport;
|
package/dist/ortoni-report.mjs
CHANGED
|
@@ -4,12 +4,12 @@ import path from "path";
|
|
|
4
4
|
import Handlebars from "handlebars";
|
|
5
5
|
import colors from "colors/safe";
|
|
6
6
|
var OrtoniReport = class {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
constructor() {
|
|
8
|
+
this.results = [];
|
|
9
|
+
}
|
|
10
10
|
onBegin(config, suite) {
|
|
11
11
|
this.results = [];
|
|
12
|
-
const screenshotsDir = path.join(
|
|
12
|
+
const screenshotsDir = path.join(process.cwd(), "screenshots");
|
|
13
13
|
if (!fs.existsSync(screenshotsDir)) {
|
|
14
14
|
fs.mkdirSync(screenshotsDir);
|
|
15
15
|
}
|
|
@@ -17,7 +17,6 @@ var OrtoniReport = class {
|
|
|
17
17
|
onTestBegin(test, result) {
|
|
18
18
|
}
|
|
19
19
|
onTestEnd(test, result) {
|
|
20
|
-
console.log(test.titlePath());
|
|
21
20
|
const testResult = {
|
|
22
21
|
projectName: test.titlePath()[1],
|
|
23
22
|
// Get the project name
|
|
@@ -38,7 +37,7 @@ var OrtoniReport = class {
|
|
|
38
37
|
filePath: test.titlePath()[2]
|
|
39
38
|
};
|
|
40
39
|
if (result.attachments) {
|
|
41
|
-
const screenshotsDir = path.join(
|
|
40
|
+
const screenshotsDir = path.join(process.cwd(), "screenshots\\" + test.id);
|
|
42
41
|
if (!fs.existsSync(screenshotsDir)) {
|
|
43
42
|
fs.mkdirSync(screenshotsDir);
|
|
44
43
|
}
|
|
@@ -46,7 +45,7 @@ var OrtoniReport = class {
|
|
|
46
45
|
if (screenshot && screenshot.path) {
|
|
47
46
|
const screenshotContent = fs.readFileSync(screenshot.path, "base64");
|
|
48
47
|
const screenshotFileName = `screenshots/${test.id}/${path.basename(screenshot.path)}`;
|
|
49
|
-
fs.writeFileSync(path.join(
|
|
48
|
+
fs.writeFileSync(path.join(process.cwd(), screenshotFileName), screenshotContent, "base64");
|
|
50
49
|
testResult.screenshotPath = screenshotFileName;
|
|
51
50
|
}
|
|
52
51
|
}
|
|
@@ -76,12 +75,12 @@ var OrtoniReport = class {
|
|
|
76
75
|
return suiteName.split(" - ");
|
|
77
76
|
});
|
|
78
77
|
const html = this.generateHTML();
|
|
79
|
-
const outputPath = path.join(
|
|
78
|
+
const outputPath = path.join(process.cwd(), "ortoni-report.html");
|
|
80
79
|
fs.writeFileSync(outputPath, html);
|
|
81
80
|
console.log(`Ortoni HTML report generated at ${outputPath}`);
|
|
82
81
|
}
|
|
83
82
|
generateHTML() {
|
|
84
|
-
const templateSource = fs.readFileSync(path.join(
|
|
83
|
+
const templateSource = fs.readFileSync(path.join(process.cwd(), "report-template.hbs"), "utf-8");
|
|
85
84
|
const template = Handlebars.compile(templateSource);
|
|
86
85
|
const data = {
|
|
87
86
|
suiteName: this.suiteName,
|
|
@@ -110,6 +109,7 @@ function safeStringify(obj, indent = 2) {
|
|
|
110
109
|
cache.clear();
|
|
111
110
|
return json;
|
|
112
111
|
}
|
|
112
|
+
var ortoni_report_default = OrtoniReport;
|
|
113
113
|
export {
|
|
114
|
-
|
|
114
|
+
ortoni_report_default as default
|
|
115
115
|
};
|
package/package.json
CHANGED
package/readme.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Release Notes: OrtoniReport v1.0.5
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
We are excited to announce the release of OrtoniReport v1.0.0, a powerful and customizable HTML report generator for Playwright tests. This release includes key features that enhance the reporting capabilities and make it easier to visualize and organize test results.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
### Enhanced Grouping and Organization
|
|
10
|
+
- **Hierarchical Grouping:** Test results are now grouped hierarchically by file name, suite name, and project name, allowing for a clear and organized view of your test structure.
|
|
11
|
+
- **Detailed Breakdown:** Each suite displays a sub-category for project names, with individual test cases listed under their respective projects.
|
|
12
|
+
|
|
13
|
+
### Configurable Report Generation
|
|
14
|
+
- **Flexible Configuration:** The reporter can be easily configured within your Playwright configuration file. Example:
|
|
15
|
+
```JS/TS
|
|
16
|
+
reporter: [
|
|
17
|
+
['ortoni-report'],
|
|
18
|
+
['dot']
|
|
19
|
+
],
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Screenshot Handling
|
|
23
|
+
- **Screenshot Storage:** Screenshots are now saved in a structured directory within the root of your project, ensuring that visual evidence is easily accessible.
|
|
24
|
+
- **Automatic Directory Creation:** The reporter automatically creates necessary directories for storing screenshots.
|
|
25
|
+
|
|
26
|
+
### Comprehensive Test Details
|
|
27
|
+
- **Rich Test Information:** Each test result includes the test title, status, duration, errors, steps, logs, and screenshot paths.
|
|
28
|
+
- **Color-coded Status:** Test statuses are color-coded (green for passed, red for failed, yellow for skipped) for quick visual identification.
|
|
29
|
+
|
|
30
|
+
### Handlebars Template Integration
|
|
31
|
+
- **Customizable Reports:** The HTML report is generated using Handlebars templates, allowing for easy customization and styling.
|
|
32
|
+
- **JSON Helper:** A custom Handlebars helper for JSON stringification is included to handle complex data structures.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
To install OrtoniReport, add it to your project using npm:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install ortoni-report --save-dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
Configure OrtoniReport in your `playwright.config.ts`:
|
|
45
|
+
|
|
46
|
+
``` javascript/typescript
|
|
47
|
+
import { defineConfig } from '@playwright/test';
|
|
48
|
+
import OrtoniReport from 'ortoni-report';
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
reporter: [
|
|
52
|
+
['ortoni-report'],
|
|
53
|
+
['dot']
|
|
54
|
+
],
|
|
55
|
+
// Other Playwright configurations
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Known Issues
|
|
60
|
+
|
|
61
|
+
- **Compatibility:** Ensure that your Node.js environment supports ES6 modules, as OrtoniReport uses modern JavaScript features.
|
|
62
|
+
|
|
63
|
+
## Future Plans
|
|
64
|
+
|
|
65
|
+
- **Additional Customization Options:** More options for customizing the appearance and structure of the HTML report.
|
|
66
|
+
- **Integration with CI/CD:** Enhanced support for continuous integration and deployment environments.
|
|
67
|
+
- **Advanced Filtering:** Additional filtering options to allow users to focus on specific subsets of test results.
|
|
68
|
+
|
|
69
|
+
## Feedback and Contributions
|
|
70
|
+
|
|
71
|
+
We welcome feedback and contributions from the community. If you encounter any issues or have suggestions for improvements, please open an issue or submit a pull request on our GitHub repository.
|
|
72
|
+
|
|
73
|
+
Thank you for using OrtoniReport! We hope it significantly enhances your Playwright testing experience.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
**LetCode with Koushik**
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Playwright Test Report</title>
|
|
8
|
+
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
|
9
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.css">
|
|
10
|
+
<style>
|
|
11
|
+
main.container {
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: 1fr 2fr;
|
|
14
|
+
gap: 20px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.header {
|
|
18
|
+
display: grid;
|
|
19
|
+
grid-template-columns: 1fr 2fr;
|
|
20
|
+
grid-column-gap: 20px;
|
|
21
|
+
grid-row-gap: 20px;
|
|
22
|
+
justify-items: stretch;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: space-evenly;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.text-success {
|
|
28
|
+
color: #28a745;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.text-danger {
|
|
32
|
+
color: #dc3545;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.sidebar {
|
|
36
|
+
border-right: 1px solid #ddd;
|
|
37
|
+
padding-right: 10px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.card {
|
|
41
|
+
padding: 1rem;
|
|
42
|
+
border: 1px solid #ddd;
|
|
43
|
+
border-radius: 0.5rem;
|
|
44
|
+
background-color: #fff;
|
|
45
|
+
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pre {
|
|
49
|
+
background-color: #f8f9fa;
|
|
50
|
+
padding: 1rem;
|
|
51
|
+
border-radius: 0.5rem;
|
|
52
|
+
overflow-x: auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.back-button {
|
|
56
|
+
display: none;
|
|
57
|
+
margin-bottom: 1rem;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
|
|
62
|
+
<body>
|
|
63
|
+
<header class="container">
|
|
64
|
+
<div class="header">
|
|
65
|
+
<div>
|
|
66
|
+
<h1>Playwright Test Report</h1>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<form role="search">
|
|
70
|
+
<input name="search" type="search" placeholder="Search" />
|
|
71
|
+
<input type="submit" value="Search" />
|
|
72
|
+
</form>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</header>
|
|
76
|
+
<main class="container">
|
|
77
|
+
<aside class="sidebar">
|
|
78
|
+
<h2>Tests</h2>
|
|
79
|
+
<div class="">
|
|
80
|
+
{{#each groupedResults}}
|
|
81
|
+
<details>
|
|
82
|
+
<summary>{{@key}}</summary>
|
|
83
|
+
<ul>
|
|
84
|
+
{{#each this}}
|
|
85
|
+
<details>
|
|
86
|
+
<summary>{{@key}}</summary>
|
|
87
|
+
<ul>
|
|
88
|
+
{{#each this}}
|
|
89
|
+
<details>
|
|
90
|
+
<summary>{{@key}}</summary>
|
|
91
|
+
<ul>
|
|
92
|
+
{{#each this}}
|
|
93
|
+
<li data-suite-name="{{suite}}" data-project-name="{{projectName}}"
|
|
94
|
+
data-test-id="{{index}}">{{title}}</li>
|
|
95
|
+
{{/each}}
|
|
96
|
+
</ul>
|
|
97
|
+
</details>
|
|
98
|
+
{{/each}}
|
|
99
|
+
</ul>
|
|
100
|
+
</details>
|
|
101
|
+
{{/each}}
|
|
102
|
+
</ul>
|
|
103
|
+
</details>
|
|
104
|
+
{{/each}}
|
|
105
|
+
</div>
|
|
106
|
+
</aside>
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
<section>
|
|
111
|
+
<div id="summary">
|
|
112
|
+
<section class="grid">
|
|
113
|
+
<div>
|
|
114
|
+
<article>
|
|
115
|
+
<header>Total Tests</header>
|
|
116
|
+
<p>{{totalCount}}</p>
|
|
117
|
+
</article>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<article>
|
|
121
|
+
<header>Passed</header>
|
|
122
|
+
<p class="text-success">{{passCount}}</p>
|
|
123
|
+
</article>
|
|
124
|
+
</div>
|
|
125
|
+
<div>
|
|
126
|
+
<article>
|
|
127
|
+
<header>Failed</header>
|
|
128
|
+
<p class="text-danger">{{failCount}}</p>
|
|
129
|
+
</article>
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
<section class="grid">
|
|
133
|
+
<div>
|
|
134
|
+
<article>
|
|
135
|
+
<header>Skipped</header>
|
|
136
|
+
<p>{{skipCount}}</p>
|
|
137
|
+
</article>
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
<article>
|
|
141
|
+
<header>Retry</header>
|
|
142
|
+
<p class="text-success">{{retryCount}}</p>
|
|
143
|
+
</article>
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
<section>
|
|
147
|
+
<h2>Test Results</h2>
|
|
148
|
+
<div class="chart-container">
|
|
149
|
+
<canvas id="testChart"></canvas>
|
|
150
|
+
</div>
|
|
151
|
+
</section>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div id="testDetails" style="display: none;">
|
|
155
|
+
<!-- Back button should be outside the dynamic content -->
|
|
156
|
+
<button class="back-button" onclick="showSummary()">Back to Summary</button>
|
|
157
|
+
<!-- Test Details will be displayed here -->
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
</main>
|
|
161
|
+
|
|
162
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
|
|
163
|
+
<script>
|
|
164
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
165
|
+
const testData = {{{ json results }}};
|
|
166
|
+
const testDetails = document.getElementById('testDetails');
|
|
167
|
+
const summary = document.getElementById('summary');
|
|
168
|
+
const backButton = document.querySelector('.back-button');
|
|
169
|
+
|
|
170
|
+
function showSummary() {
|
|
171
|
+
summary.style.display = 'block';
|
|
172
|
+
testDetails.style.display = 'none';
|
|
173
|
+
backButton.style.display = 'none';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
window.showSummary = showSummary;
|
|
177
|
+
|
|
178
|
+
function displayTestDetails(test) {
|
|
179
|
+
summary.style.display = 'none';
|
|
180
|
+
testDetails.style.display = 'block';
|
|
181
|
+
backButton.style.display = 'block';
|
|
182
|
+
testDetails.innerHTML = `
|
|
183
|
+
<button class="back-button" style="display: block" onclick="showSummary()">Back to Summary</button>
|
|
184
|
+
<h2>${test.title}</h2>
|
|
185
|
+
<div class="grid">
|
|
186
|
+
<div>
|
|
187
|
+
<h3>Status</h3>
|
|
188
|
+
<p class="${test.status === 'passed' ? 'text-success' : 'text-danger'}">${test.status}</p>
|
|
189
|
+
<h3>Duration</h3>
|
|
190
|
+
<p>${test.duration}ms</p>
|
|
191
|
+
${test.errors.length ? `
|
|
192
|
+
<h3>Errors</h3>
|
|
193
|
+
<pre>${test.errors.join('\n')}</pre>
|
|
194
|
+
` : ''}
|
|
195
|
+
</div>
|
|
196
|
+
<div>
|
|
197
|
+
${test.screenshotPath ? `
|
|
198
|
+
<h3>Screenshot</h3>
|
|
199
|
+
<img src="${test.screenshotPath}" alt="Screenshot">
|
|
200
|
+
` : ''}
|
|
201
|
+
${test.logs ? `
|
|
202
|
+
<h3>Logs</h3>
|
|
203
|
+
<pre>${test.logs}</pre>
|
|
204
|
+
` : ''}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const testItems = document.querySelectorAll('[data-test-id]');
|
|
211
|
+
testItems.forEach(item => {
|
|
212
|
+
item.addEventListener('click', () => {
|
|
213
|
+
const testId = item.getAttribute('data-test-id');
|
|
214
|
+
const test = testData[testId];
|
|
215
|
+
displayTestDetails(test);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const ctx = document.getElementById('testChart').getContext('2d');
|
|
220
|
+
new Chart(ctx, {
|
|
221
|
+
type: 'doughnut',
|
|
222
|
+
data: {
|
|
223
|
+
labels: ['Passed', 'Failed', 'Skipped'],
|
|
224
|
+
datasets: [{
|
|
225
|
+
data: [{{ passCount }}, {{ failCount }}, {{ skipCount }}],
|
|
226
|
+
backgroundColor: ['#28a745', '#dc3545', '#f0f662']
|
|
227
|
+
}]
|
|
228
|
+
},
|
|
229
|
+
options: {
|
|
230
|
+
responsive: true,
|
|
231
|
+
maintainAspectRatio: false,
|
|
232
|
+
plugins: {
|
|
233
|
+
title: {
|
|
234
|
+
display: true,
|
|
235
|
+
text: 'Overall',
|
|
236
|
+
font: {
|
|
237
|
+
size: 18,
|
|
238
|
+
weight: 'bold'
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
legend: {
|
|
242
|
+
position: 'right'
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
</script>
|
|
249
|
+
</body>
|
|
250
|
+
|
|
251
|
+
</html>
|