diff --git a/app/components/stats.hbs b/app/components/stats.hbs new file mode 100644 index 0000000..b6ad9fc --- /dev/null +++ b/app/components/stats.hbs @@ -0,0 +1,123 @@ +
+ Stats +
+ {{#if this.removedToday.size}} +
+ 🎉🌅 Rules removed today + +
    + {{#each this.removedToday as |rule|}} +
  • + {{rule}} +
  • + {{/each}} +
+
+ {{/if}} + + {{#if this.removedThisWeek.size}} +
+ 🎉🗓️ Rules removed this week + +
    + {{#each this.removedThisWeek as |rule|}} +
  • + {{rule}} +
  • + {{/each}} +
+
+ {{/if}} + + {{#if this.improvedToday}} +
+ 📉🌅 Biggest improvement since yesterday: +
+ {{this.improvedToday.rule}}
+ which has removed + {{absolute this.improvedToday.value}} + files +
+ {{/if}} + + {{#if this.improvedThisWeek}} +
+ 📉🗓️ Biggest improvment this week: + +
+ + {{this.improvedThisWeek.rule}} +
+ which has removed + {{absolute this.improvedThisWeek.value}} + files + +
+ {{/if}} +
+ +
+ {{#if this.newToday.size}} +
+ 🆕🌅 New rules added since yesterday: + +
    + {{#each this.newToday as |rule|}} +
  • {{rule}}
  • + {{/each}} +
+
+ {{/if}} + + {{#if this.newThisWeek.size}} +
+ 🆕🗓️ New rules added this week: + +
    + {{#each this.newThisWeek as |rule|}} +
  • {{rule}}
  • + {{/each}} +
+
+ {{/if}} + + {{#if this.mostAddedToday}} +
+ 📈🌅 Most new files since yesterday: + +
+ {{this.mostAddedToday.rule}} +
+ + which added + {{this.mostAddedToday.value}} + files + +
+ {{/if}} + + {{#if this.mostAddedThisWeek}} +
+ 📈🗓️ Most new files 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..1b964d9 --- /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.newToday.has(rule) || 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/helpers/absolute.js b/app/helpers/absolute.js new file mode 100644 index 0000000..a6b9387 --- /dev/null +++ b/app/helpers/absolute.js @@ -0,0 +1,5 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function absolute(positional /*, named*/) { + return Math.abs(positional); +}); 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..2a387bf 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -20,10 +20,28 @@ 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; +} + +.stat-card ul { + padding-left: inherit; +} + .graphs { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(600px, 100%), 1fr)); - grid-gap: 20px + grid-gap: 20px; } .lttf-chart { @@ -102,9 +120,11 @@ details > .graphs { a { color: #FBBF24; } - .lttf-chart { + + .lttf-chart, .stat-card { background-color: #16213E; } + .chart-container .axis { fill: white !important; } @@ -129,3 +149,9 @@ footer { .frappe-chart .y-markers { display: none; } + +/* helper classes */ +.my-2 { + margin-top: .5em; + margin-bottom: .5em; +} 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/acceptance/basic-test.js b/tests/acceptance/basic-test.js index 37fb7d6..a13ce04 100644 --- a/tests/acceptance/basic-test.js +++ b/tests/acceptance/basic-test.js @@ -10,7 +10,7 @@ module('Acceptance | basic', function(hooks) { await visit('/'); assert.equal(currentURL(), '/'); - assert.dom('[data-test-chart]').exists({count: 10}) + assert.dom('[data-test-chart]').exists({count: 11}) // await this.pauseTest(); assert.dom('[data-test-chart="lint-to-the-future-ember-template:require-valid-alt-text"] [data-test-time-button="monthly"]').isChecked(); @@ -29,6 +29,6 @@ module('Acceptance | basic', function(hooks) { // top level charts assert.dom('.ember-application > div > [data-test-chart]').exists({count: 9}) - assert.dom('[data-test-completed-rules] [data-test-chart]').exists({count: 1}); + assert.dom('[data-test-completed-rules] [data-test-chart]').exists({count: 2}); }) });