From 7df2f17a420f1ef5319562024ee5ab05266fca7a Mon Sep 17 00:00:00 2001 From: Chris Manson Date: Thu, 12 Dec 2024 18:02:53 +0000 Subject: [PATCH] add basic stats --- app/components/stats.hbs | 113 ++++++++++++++++++++ app/components/stats.js | 114 +++++++++++++++++++++ app/routes/application.js | 80 +++++++++++++-- app/styles/app.css | 20 +++- app/templates/index.hbs | 2 + package.json | 3 +- pnpm-lock.yaml | 12 +++ server/mocks/data.json | 25 ++++- tests/integration/components/stats-test.js | 26 +++++ 9 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 app/components/stats.hbs create mode 100644 app/components/stats.js create mode 100644 tests/integration/components/stats-test.js diff --git a/app/components/stats.hbs b/app/components/stats.hbs new file mode 100644 index 0000000..88bfc59 --- /dev/null +++ b/app/components/stats.hbs @@ -0,0 +1,113 @@ +
+ {{#if this.removedToday}} +
+ Rules removed today + +
    + {{#each this.removedToday as |rule|}} +
  • + {{rule}} +
  • + {{/each}} +
+
+ {{/if}} + + {{#if this.removedThisWeek}} +
+ Rules removed this week + +
    + {{#each this.removedThisWeek as |rule|}} +
  • + {{rule}} +
  • + {{/each}} +
+
+ {{/if}} + + + {{#if this.improvedToday}} +
+ Biggest improver since yesterday +
    +
  • + + {{this.improvedToday.rule}} which has improved by {{this.improvedToday.value}} +
  • +
+
+ {{/if}} + + {{#if this.improvedThisWeek}} +
+ Biggest improver this week: + +
    +
  • + + {{this.improvedThisWeek.rule}} which has improved by {{this.improvedThisWeek.value}} +
  • +
+ +
+ {{/if}} +
+ +
+ {{#if this.newToday}} +
+ New rules added since yesterday: + +
    + {{#each this.newToday as |rule|}} + {{rule}} + {{/each}} +
+
+ {{/if}} + + + {{#if this.newThisWeek}} +
+ New rules added this week: + +
    + {{#each this.newThisWeek as |rule|}} + {{rule}} + {{/each}} +
+
+ {{/if}} + + {{#if this.mostAddedToday}} +
+ Rule with most files added since yesterday: + +
    +
  • + + {{this.mostAddedThisWeek.rule}} which added {{this.mostAddedToday.value}} files +
  • +
+ + +
+ {{/if}} + + {{#if this.mostAddedThisWeek}} +
+ Rule with most files added this week: + +
    +
  • + + {{this.mostAddedThisWeek.rule}} which added {{this.mostAddedThisWeek.value}} files +
  • +
+ + +
+ {{/if}} +
\ No newline at end of file diff --git a/app/components/stats.js b/app/components/stats.js new file mode 100644 index 0000000..af8dfbd --- /dev/null +++ b/app/components/stats.js @@ -0,0 +1,114 @@ +import Component from '@glimmer/component'; + +export default class Stats extends Component { + get improvedToday() { + let biggest; + + for (let [rule, value] of Object.entries(this.args.data?.today?.changed)) { + if (value > 0) { + continue; + } + // removing trumps improvement + if (this.removedToday.has(rule)) { + continue; + } + // remember smaller numbers are bigger "improvmeents"; + if (!biggest || value < biggest.value) { + biggest = { rule, value }; + } + } + return biggest; + } + + get improvedThisWeek() { + let biggest; + + for (let [rule, value] of Object.entries( + this.args.data?.thisWeek?.changed, + )) { + if ( + value > 0 || + rule === this.improvedToday?.rule || + value >= this.improvedToday?.value + ) { + continue; + } + + // removing trumps improvement + if (this.removedThisWeek.has(rule)) { + continue; + } + + // remember smaller numbers are bigger "improvmeents"; + if (!biggest || value < biggest.value) { + biggest = { rule, value }; + } + } + + return biggest; + } + + get mostAddedToday() { + let biggest; + + for (let [rule, value] of Object.entries(this.args.data?.today?.changed)) { + if (value < 0) { + continue; + } + // new trumps added + if (this.newToday.has(rule)) { + continue; + } + + if (!biggest || value > biggest.value) { + biggest = { rule, value }; + } + } + return biggest; + } + + get mostAddedThisWeek() { + let biggest; + + for (let [rule, value] of Object.entries( + this.args.data?.thisWeek?.changed, + )) { + if ( + value < 0 || + rule === this.mostAddedToday?.rule || + value <= this.mostAddedToday?.value + ) { + continue; + } + + // new trumps added + if (this.newThisWeek.has(rule)) { + continue; + } + + if (!biggest || value > biggest.value) { + biggest = { rule, value }; + } + } + + return biggest; + } + + get newToday() { + return new Set(this.args.data?.today?.added); + } + + get newThisWeek() { + return new Set(this.args.data.thisWeek.added).difference(this.newToday); + } + + get removedToday() { + return new Set(this.args.data?.today?.removed); + } + + get removedThisWeek() { + return new Set(this.args.data.thisWeek.removed).difference( + this.removedToday, + ); + } +} diff --git a/app/routes/application.js b/app/routes/application.js index 40025b5..b73e6a2 100644 --- a/app/routes/application.js +++ b/app/routes/application.js @@ -1,30 +1,98 @@ /* eslint-disable prettier/prettier */ import Route from '@ember/routing/route'; import fetch from 'fetch'; +import { Temporal } from 'temporal-polyfill' import env from 'lint-to-the-future/config/environment'; import timeSeries from 'lint-to-the-future/utils/time-series'; +function lengthOrValuOrZero(data) { + if(!data) { + return 0; + } + return data.length ?? data; +} + +function compareData(data, today, past) { + const timeSeriesData = timeSeries(data); + + let changed = {} + let removed = []; + let added = []; + + for(let rule in timeSeriesData) { + let diff = lengthOrValuOrZero(timeSeriesData[rule][today]) - lengthOrValuOrZero(timeSeriesData[rule][past]); + + if (diff !== 0 ) { + changed[rule] = diff; + } + + if (!timeSeriesData[rule][today] && timeSeriesData[rule][past]) { + removed.push(rule); + } + + if (timeSeriesData[rule][today] && !timeSeriesData[rule][past]) { + added.push(rule); + } + } + return { + changed, + removed, + added, + } +} + export default class ApplicationRoute extends Route { async model() { let data = await (await fetch(`${env.rootURL}data.json`)).json(); + let allDates = Object.keys(data).sort((a, b) => b.localeCompare(a)) + let timeSeriesData = timeSeries(data); - let highestDate; + const globalHighestDate = allDates[0]; + const stats = {} + + const today = Temporal.PlainDate.from(globalHighestDate); + + // there is at least another date in the data + if (allDates[1]) { + const yesterday = Temporal.PlainDate.from(allDates[1]); + if (yesterday.until(today).days === 1) { + // there was a yesterday + stats.today = compareData({ + [globalHighestDate]: data[globalHighestDate], + [allDates[1]]: data[allDates[1]] + }, globalHighestDate, allDates[1]) + } + + let lastWeek = yesterday; + + for (let i = 2; i < allDates.length; i++) { + const currentDate = Temporal.PlainDate.from(allDates[i]); + if (currentDate.until(today).days > 7) { + break; + } - for (const rule in timeSeriesData) { - for (const date in timeSeriesData[rule]) { - if(!highestDate || highestDate < date) { - highestDate = date; + if (currentDate.until(lastWeek).days > 0) { + lastWeek = Temporal.PlainDate.from(currentDate) } } + + // if we have a date that is bigger than yesterday but not bigger than 7 days ago + if (lastWeek !== yesterday) { + stats.thisWeek = compareData({ + [globalHighestDate]: data[globalHighestDate], + [lastWeek.toString()]: data[lastWeek.toString()] + }, globalHighestDate, lastWeek.toString()) + } } return { data: timeSeriesData, - highestDate, + highestDate: globalHighestDate, + stats, } } } diff --git a/app/styles/app.css b/app/styles/app.css index 04c0ca9..45a24e2 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -20,10 +20,24 @@ a { color: black; } +.stats-wrapper { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); + grid-gap: 20px; + margin-bottom: 20px; +} + +.stat-card { + background-color: white; + border-radius: var(--chart-border-radius); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + padding: 20px; +} + .graphs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(600px, 100%), 1fr)); - grid-gap: 20px + grid-gap: 20px; } .lttf-chart { @@ -102,9 +116,11 @@ details > .graphs { a { color: #FBBF24; } - .lttf-chart { + + .lttf-chart, .stat-card { background-color: #16213E; } + .chart-container .axis { fill: white !important; } diff --git a/app/templates/index.hbs b/app/templates/index.hbs index b33ca6e..5123358 100644 --- a/app/templates/index.hbs +++ b/app/templates/index.hbs @@ -1,3 +1,5 @@ + +
{{#each-in this.rules.rulesToComplete as |key value|}} diff --git a/package.json b/package.json index b7b093e..e3c5462 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "commander": "^9.4.1", "fs-extra": "^7.0.1", "import-cwd": "^3.0.0", - "node-fetch": "^2.6.0" + "node-fetch": "^2.6.0", + "temporal-polyfill": "^0.2.5" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ca9e9d..8c29d04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,7 @@ specifiers: stylelint-config-standard: ^34.0.0 stylelint-prettier: ^4.1.0 temp: ^0.9.4 + temporal-polyfill: ^0.2.5 tracked-built-ins: ^3.3.0 webpack: ^5.93.0 @@ -76,6 +77,7 @@ dependencies: fs-extra: 7.0.1 import-cwd: 3.0.0 node-fetch: 2.7.0 + temporal-polyfill: 0.2.5 devDependencies: '@babel/core': 7.25.2 @@ -12863,6 +12865,16 @@ packages: rimraf: 2.6.3 dev: true + /temporal-polyfill/0.2.5: + resolution: {integrity: sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==} + dependencies: + temporal-spec: 0.2.4 + dev: false + + /temporal-spec/0.2.4: + resolution: {integrity: sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==} + dev: false + /terser-webpack-plugin/5.3.10_webpack@5.95.0: resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} engines: {node: '>= 10.13.0'} diff --git a/server/mocks/data.json b/server/mocks/data.json index f51bf81..e8c8dd0 100644 --- a/server/mocks/data.json +++ b/server/mocks/data.json @@ -126,12 +126,27 @@ "ember/no-empty-glimmer-component-classes": ["addon/components/es-link-card.js"], "ember/no-classic-components": ["addon/components/es-header-navbar-link.js"], "ember/no-classic-classes": ["addon/components/es-header-navbar-link.js", "addon/services/navbar.js", "tests/dummy/app/components/link-to.js"], - "ember/require-tagless-components": ["addon/components/es-header-navbar-link.js", "tests/dummy/app/components/link-to.js"], + "ember/require-tagless-components": 10, "ember/no-get": ["addon/components/es-header-navbar-link.js"], "ember/no-component-lifecycle-hooks": ["addon/components/es-header-navbar-link.js"], "ember/require-super-in-lifecycle-hooks": ["addon/components/es-header-navbar-link.js"] } }, + "2021-04-09": { + "lint-to-the-future-ember-template": { + "require-valid-alt-text": 2, + "no-class:strange-name": 1 + }, + "lint-to-the-future-eslint": { + "prettier/prettier": 24, + "ember/no-classic-components": 1, + "ember/no-classic-classes": 6, + "ember/require-tagless-components": 2, + "ember/no-get": 1, + "ember/no-component-lifecycle-hooks": 1, + "ember/require-super-in-lifecycle-hooks": 8 + } + }, "2021-04-10": { "lint-to-the-future-ember-template": { "require-valid-alt-text": ["addon/templates/components/es-card-content.hbs"], @@ -139,12 +154,12 @@ }, "lint-to-the-future-eslint": { "prettier/prettier": ["addon/components/es-button.js", "addon/components/es-card-content.js", "addon/components/es-card.js", "addon/components/es-footer-contributions.js", "addon/components/es-footer-help.js", "addon/components/es-footer-info.js", "addon/components/es-footer-statement.js", "addon/components/es-footer.js", "addon/components/es-header-navbar-link.js", "addon/components/es-link-card.js", "addon/components/es-note.js", "addon/constants/es-footer.js", "addon/services/navbar.js", "app/components/es-button.js", "app/components/es-card-content.js", "app/components/es-card.js", "app/components/es-footer.js", "app/components/es-header.js", "app/components/es-link-card.js", "app/components/es-note.js", "ember-cli-build.js", "index.js", "tests/acceptance/visual-regression-test-test.js", "tests/dummy/app/components/link-to.js", "tests/dummy/app/helpers/increment.js", "tests/dummy/config/environment.js", "tests/integration/components/es-button-test.js", "tests/integration/components/es-card-test.js", "tests/integration/components/es-footer-test.js", "tests/integration/components/es-header-test.js", "tests/integration/components/es-note-test.js", "tests/integration/components/es-progress-bar-test.js"], - "ember/no-classic-components": ["addon/components/es-header-navbar-link.js"], - "ember/no-classic-classes": ["addon/components/es-header-navbar-link.js", "addon/services/navbar.js", "tests/dummy/app/components/link-to.js"], + "ember/no-classic-components": ["addon/components/es-header-navbar-link.js", "addon/components/es-button.js", "addon/components/es-card-content.js", "addon/components/es-card.js", "addon/components/es-footer-contributions.js", "addon/components/es-footer-help.js"], + "ember/no-classic-classes": ["addon/components/es-header-navbar-link.js", "addon/services/navbar.js", "tests/dummy/app/components/link-to.js", "addon/components/es-button.js", "addon/components/es-card-content.js", "addon/components/es-card.js", "addon/components/es-footer-contributions.js", "addon/components/es-footer-help.js", "addon/components/es-footer-info.js", "addon/components/es-footer-statement.js", "addon/components/es-footer.js", "addon/components/es-header-navbar-link.js"], "ember/require-tagless-components": ["addon/components/es-header-navbar-link.js", "tests/dummy/app/components/link-to.js"], + "completely-new-rule/only-today-ever": ["addon/components/es-header-navbar-link.js", "tests/dummy/app/components/link-to.js"], "ember/no-get": ["addon/components/es-header-navbar-link.js"], - "ember/no-component-lifecycle-hooks": ["addon/components/es-header-navbar-link.js"], - "ember/require-super-in-lifecycle-hooks": ["addon/components/es-header-navbar-link.js"] + "ember/no-component-lifecycle-hooks": ["addon/components/es-header-navbar-link.js"] } } } diff --git a/tests/integration/components/stats-test.js b/tests/integration/components/stats-test.js new file mode 100644 index 0000000..99b30c4 --- /dev/null +++ b/tests/integration/components/stats-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'lint-to-the-future/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | stats', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +});