diff --git a/src/data/reviews.json.ts b/src/data/reviews.json.ts index 7dbc58d..1e2ce79 100644 --- a/src/data/reviews.json.ts +++ b/src/data/reviews.json.ts @@ -2,8 +2,13 @@ import { HttpClient, Terminal } from '@effect/platform' import { NodeTerminal } from '@effect/platform-node' import { Schema } from '@effect/schema' import { Config, Effect, Redacted } from 'effect' +import * as Temporal from '../lib/Temporal.js' -const Reviews = Schema.Array(Schema.Struct({})) +const Reviews = Schema.Array( + Schema.Struct({ + createdAt: Temporal.PlainDateFromStringSchema, + }), +) const program = Effect.gen(function* () { const terminal = yield* Terminal.Terminal diff --git a/src/lib/Temporal.ts b/src/lib/Temporal.ts index fb063f8..da382e2 100644 --- a/src/lib/Temporal.ts +++ b/src/lib/Temporal.ts @@ -1,12 +1,15 @@ import { ParseResult, Schema } from '@effect/schema' import { Temporal } from '@js-temporal/polyfill' -export const { Instant } = Temporal +export const { Instant, PlainDate } = Temporal export type Instant = Temporal.Instant +export type PlainDate = Temporal.PlainDate export const InstantFromSelfSchema = Schema.instanceOf(Temporal.Instant) +export const PlainDateFromSelfSchema = Schema.instanceOf(PlainDate) + export const InstantFromStringSchema: Schema.Schema = Schema.transformOrFail( Schema.String, InstantFromSelfSchema, @@ -19,3 +22,16 @@ export const InstantFromStringSchema: Schema.Schema = Schema.tr encode: instant => ParseResult.succeed(instant.toString()), }, ) + +export const PlainDateFromStringSchema: Schema.Schema = Schema.transformOrFail( + Schema.String, + PlainDateFromSelfSchema, + { + decode: (date, _, ast) => + ParseResult.try({ + try: () => PlainDate.from(date, { overflow: 'reject' }), + catch: () => new ParseResult.Type(ast, date), + }), + encode: plainDate => ParseResult.succeed(plainDate.toString()), + }, +) diff --git a/src/reviews.md b/src/reviews.md index 534ec05..13ba945 100644 --- a/src/reviews.md +++ b/src/reviews.md @@ -7,7 +7,16 @@ toc: false # PREreviews ✍️ ```js -const reviews = FileAttachment('./data/reviews.json').json() +const parseDate = d3.utcParse('%Y-%m-%d') + +const reviews = FileAttachment('./data/reviews.json') + .json() + .then(data => data.map(review => ({ ...review, createdAt: parseDate(review.createdAt) }))) +``` + +```js +const now = new Date() +const firstReview = d3.min(reviews, review => review.createdAt) ```
@@ -16,3 +25,34 @@ const reviews = FileAttachment('./data/reviews.json').json() ${reviews.length.toLocaleString("en-US")}
+ +```js +function reviewsTimeline({ width } = {}) { + return Plot.plot({ + title: 'PREreviews per month', + width: Math.max(width, 600), + height: 400, + y: { grid: true, label: 'PREreviews', tickFormat: Math.floor, interval: 1 }, + x: { label: '', domain: [d3.utcMonth.floor(firstReview), d3.utcMonth.ceil(now)] }, + marks: [ + Plot.rectY( + reviews, + Plot.binX( + { y: 'count' }, + { + x: 'createdAt', + interval: d3.utcMonth, + tip: true, + }, + ), + ), + ], + }) +} +``` + +
+
+ ${resize((width) => reviewsTimeline({width}))} +
+