diff --git a/README.md b/README.md index c65a6c7..f085fa7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![alt text](./graph.png) + # Reading Analytics System Analytics system of my reading progress diff --git a/graph.png b/graph.png new file mode 100644 index 0000000..b3aa7c8 Binary files /dev/null and b/graph.png differ diff --git a/ras/api/urls.py b/ras/api/urls.py index 6982d8e..0e2ae9e 100644 --- a/ras/api/urls.py +++ b/ras/api/urls.py @@ -2,6 +2,6 @@ from django.urls import path from .views import * urlpatterns = [ - path('books', api_all_books), - path('books/genres', books_per_genre_per_month) + path('books/genres', books_per_genre_per_month), + path('ratings', avg_ratings_per_month) ] \ No newline at end of file diff --git a/ras/api/views.py b/ras/api/views.py index 3cfee69..6d878cb 100644 --- a/ras/api/views.py +++ b/ras/api/views.py @@ -8,22 +8,6 @@ from django.db.models import Q from django.templatetags.static import static import json -@api_view(['GET']) -def api_all_books(request): - df = pd.read_csv("api/static/books2.csv", encoding = "utf-8") - books = [] - for book in df['Books']: - info = book.split(';') - books.append({ - "name": info[0], - "author": info[1], - "genre": info[2], - "pages": info[3], - "readed": info[4], - "rating": info[5] - }) - return Response(books) - @api_view(['GET']) def books_per_genre_per_month(request): @@ -50,6 +34,32 @@ def books_per_genre_per_month(request): "count": row['count'] }) + return Response(data) + else: + return Response("No year header included") + +@api_view(['GET']) +def avg_ratings_per_month(request): + datayear = request.META.get('HTTP_YEAR') + + if datayear: + data = [] + + # Get CSV file with book data + df = pd.read_csv("api/static/books2.csv", encoding = "utf-8", header = 0, sep=';') + + # Filter data on year + df = df.where(df['readed'].str.contains(datayear)) + + avgratingspermonth = df.groupby('readed')['rating'].mean().reset_index(name="rating") + + for index, row in avgratingspermonth.iterrows(): + + data.append({ + "date": row['readed'], + "rating": int(row['rating']) + }) + return Response(data) else: return Response("No year header included") \ No newline at end of file diff --git a/ras/frontend/src/App.js b/ras/frontend/src/App.js index 22b4c0f..4f1a1e2 100644 --- a/ras/frontend/src/App.js +++ b/ras/frontend/src/App.js @@ -5,15 +5,21 @@ export default class App extends Component { super(props); } - initChart(data, year) { + initChart(data, ratings, year) { + + /* + ---------------------------------- + Books per month per genre + ---------------------------------- + */ var genres = []; var colors = [ - '#d0b2cf', '#b66f2b', '#003C72', '#ecdb0e' + '#696ffc', '#7596fa', '#92adfe', '#abc0ff' ] - var dataSet = []; + var dataSet = []; data.forEach(book => { if (!genres.includes(book.genre)) { @@ -48,11 +54,51 @@ export default class App extends Component { dataSet.push({ label: genre, data: genreData, - backgroundColor: colors[index] + backgroundColor: colors[index], + order: 2 }) }) } + /* + ---------------------------------- + Avarage ratings per month + ---------------------------------- + */ + + var avgRatings = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + for (var j = 0; j < 12; j++) { + + if (j < 9) { + var month = "0" + (j + 1) + } else { + month = (j + 1) + } + + for (var i = 0; i < ratings.length; i++) { + if (ratings[i].date == month + '-' + year) { + avgRatings[j] = ratings[i].rating; + } + } + } + + dataSet.push({ + label: 'Gemiddelde beoordeling', + data: avgRatings, + backgroundColor: '#ffa500', + borderColor: '#ffa500', + tension: 0.4, + type: 'line', + order: 1 + }) + + /* + ---------------------------------- + Stacked bar chart + ---------------------------------- + */ + new Chart(document.getElementById('chart'), { type: 'bar', data: { @@ -60,29 +106,37 @@ export default class App extends Component { datasets: dataSet }, options: { + responsive: true, + showTooltips: true, legend: { display: true, }, - tooltips: { - mode: 'index', - intersect: true, - axis: 'y' + interaction: { + mode: 'index' }, scales: { x: { ticks: { beginAtZero: true, - + color: "white", }, stacked: true, }, y: { ticks: { beginAtZero: true, - stepSize: 1 + stepSize: 1, + color: "white", }, stacked: true } + }, + plugins: { + legend: { + labels: { + color: "white" + } + } } } }); @@ -90,9 +144,7 @@ export default class App extends Component { componentDidMount() { - var currentyear = new Date().getFullYear() - - console.log(currentyear); + var currentyear = new Date("2021-09-29").getFullYear() fetch('/api/books/genres', { "method": "GET", @@ -101,17 +153,26 @@ export default class App extends Component { } }) .then(response => response.json()) - .then(data => { - this.initChart(data, currentyear); - console.log(data); + .then(books => { + fetch('/api/ratings', { + "method": "GET", + "headers": { + "year": currentyear + } + }) + .then(response => response.json()) + .then(ratings => { + console.log(ratings); + this.initChart(books, ratings, currentyear); + }) + }) } render() { return ( - -

Soon here comes the Reading Analytics System! test

+
) diff --git a/ras/frontend/static/js/main.js b/ras/frontend/static/js/main.js index 0d564f8..60fa102 100644 --- a/ras/frontend/static/js/main.js +++ b/ras/frontend/static/js/main.js @@ -1,2 +1,2 @@ /*! For license information please see main.js.LICENSE.txt */ -(()=>{"use strict";var __webpack_modules__={"./src/App.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "default": () => (/* binding */ App)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nclass App extends react__WEBPACK_IMPORTED_MODULE_0__.Component {\n constructor(props) {\n super(props);\n }\n\n initChart(data, year) {\n var genres = [];\n var colors = [\'#d0b2cf\', \'#b66f2b\', \'#003C72\', \'#ecdb0e\'];\n var dataSet = [];\n data.forEach(book => {\n if (!genres.includes(book.genre)) {\n genres.push(book.genre);\n }\n });\n\n if (genres && genres.length > 0) {\n genres.forEach((genre, index) => {\n var genreData = [];\n\n for (var i = 0; i < 12; i++) {\n genreData[i] = 0;\n\n if (i + 1 < 10) {\n var month = "0" + (i + 1);\n } else {\n month = i + 1;\n }\n\n for (var j = 0; j < data.length; j++) {\n if (data && data[j] && data[j].readed == month + \'-\' + year) {\n if (data[j].genre == genre) {\n genreData[i] = data[j].count;\n }\n }\n }\n }\n\n dataSet.push({\n label: genre,\n data: genreData,\n backgroundColor: colors[index]\n });\n });\n }\n\n new Chart(document.getElementById(\'chart\'), {\n type: \'bar\',\n data: {\n labels: ["Januari", "Februari", "Maart", "April", "Mei", "Juni", "Juli", "Augustus", "September", "Oktober", "November", "December"],\n datasets: dataSet\n },\n options: {\n legend: {\n display: true\n },\n tooltips: {\n mode: \'index\',\n intersect: true,\n axis: \'y\'\n },\n scales: {\n x: {\n ticks: {\n beginAtZero: true\n },\n stacked: true\n },\n y: {\n ticks: {\n beginAtZero: true,\n stepSize: 1\n },\n stacked: true\n }\n }\n }\n });\n }\n\n componentDidMount() {\n var currentyear = new Date().getFullYear();\n console.log(currentyear);\n fetch(\'/api/books/genres\', {\n "method": "GET",\n "headers": {\n "year": currentyear\n }\n }).then(response => response.json()).then(data => {\n this.initChart(data, currentyear);\n console.log(data);\n });\n }\n\n render() {\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("canvas", {\n id: "chart"\n }), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", null, "Soon here comes the Reading Analytics System! test"));\n }\n\n}\n\n//# sourceURL=webpack://frontend/./src/App.js?')},"./src/index.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{eval('__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "./node_modules/react/index.js");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-dom */ "./node_modules/react-dom/index.js");\n/* harmony import */ var _App__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App */ "./src/App.js");\n\n\n\nreact_dom__WEBPACK_IMPORTED_MODULE_1__.render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_App__WEBPACK_IMPORTED_MODULE_2__["default"], null), document.getElementById(\'app\'));\n\n//# sourceURL=webpack://frontend/./src/index.js?')},"./node_modules/react-dom/cjs/react-dom.development.js":(__unused_webpack_module,exports,__webpack_require__)=>{eval("/**\n * @license React\n * react-dom.development.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\n\nif (true) {\n (function() {\n\n 'use strict';\n\n/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */\nif (\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' &&\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart ===\n 'function'\n) {\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());\n}\n var React = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\nvar Scheduler = __webpack_require__(/*! scheduler */ \"./node_modules/scheduler/index.js\");\n\nvar ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n\nvar suppressWarning = false;\nfunction setSuppressWarning(newSuppressWarning) {\n {\n suppressWarning = newSuppressWarning;\n }\n} // In DEV, calls to console.warn and console.error get replaced\n// by calls to these methods by a Babel plugin.\n//\n// In PROD (or in packages without access to React internals),\n// they are left as they are instead.\n\nfunction warn(format) {\n {\n if (!suppressWarning) {\n for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n args[_key - 1] = arguments[_key];\n }\n\n printWarning('warn', format, args);\n }\n }\n}\nfunction error(format) {\n {\n if (!suppressWarning) {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n\n printWarning('error', format, args);\n }\n }\n}\n\nfunction printWarning(level, format, args) {\n // When changing this logic, you might want to also\n // update consoleWithStackDev.www.js as well.\n {\n var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;\n var stack = ReactDebugCurrentFrame.getStackAddendum();\n\n if (stack !== '') {\n format += '%s';\n args = args.concat([stack]);\n } // eslint-disable-next-line react-internal/safe-string-coercion\n\n\n var argsWithFormat = args.map(function (item) {\n return String(item);\n }); // Careful: RN currently depends on this prefix\n\n argsWithFormat.unshift('Warning: ' + format); // We intentionally don't use spread (or .apply) directly because it\n // breaks IE9: https://github.com/facebook/react/issues/13610\n // eslint-disable-next-line react-internal/no-production-logging\n\n Function.prototype.apply.call(console[level], console, argsWithFormat);\n }\n}\n\nvar FunctionComponent = 0;\nvar ClassComponent = 1;\nvar IndeterminateComponent = 2; // Before we know whether it is function or class\n\nvar HostRoot = 3; // Root of a host tree. Could be nested inside another node.\n\nvar HostPortal = 4; // A subtree. Could be an entry point to a different renderer.\n\nvar HostComponent = 5;\nvar HostText = 6;\nvar Fragment = 7;\nvar Mode = 8;\nvar ContextConsumer = 9;\nvar ContextProvider = 10;\nvar ForwardRef = 11;\nvar Profiler = 12;\nvar SuspenseComponent = 13;\nvar MemoComponent = 14;\nvar SimpleMemoComponent = 15;\nvar LazyComponent = 16;\nvar IncompleteClassComponent = 17;\nvar DehydratedFragment = 18;\nvar SuspenseListComponent = 19;\nvar ScopeComponent = 21;\nvar OffscreenComponent = 22;\nvar LegacyHiddenComponent = 23;\nvar CacheComponent = 24;\nvar TracingMarkerComponent = 25;\n\n// -----------------------------------------------------------------------------\n\nvar enableClientRenderFallbackOnTextMismatch = true; // TODO: Need to review this code one more time before landing\n// the react-reconciler package.\n\nvar enableNewReconciler = false; // Support legacy Primer support on internal FB www\n\nvar enableLazyContextPropagation = false; // FB-only usage. The new API has different semantics.\n\nvar enableLegacyHidden = false; // Enables unstable_avoidThisFallback feature in Fiber\n\nvar enableSuspenseAvoidThisFallback = false; // Enables unstable_avoidThisFallback feature in Fizz\n// React DOM Chopping Block\n//\n// Similar to main Chopping Block but only flags related to React DOM. These are\n// grouped because we will likely batch all of them into a single major release.\n// -----------------------------------------------------------------------------\n// Disable support for comment nodes as React DOM containers. Already disabled\n// in open source, but www codebase still relies on it. Need to remove.\n\nvar disableCommentsAsDOMContainers = true; // Disable javascript: URL strings in href for XSS protection.\n// and client rendering, mostly to allow JSX attributes to apply to the custom\n// element's object properties instead of only HTML attributes.\n// https://github.com/facebook/react/issues/11347\n\nvar enableCustomElementPropertySupport = false; // Disables children for