Avatar

@fil

13 likes5 public vals
Joined January 27, 2023

Becker’s Barley trellis

SSR chart with Observable Plot

This chart is rendered server-side by val.town, using Observable Plot, from data loaded from the GitHub API. For a more complete example, see https://www.val.town/v/fil.earthquakes. For information on this chart, see https://observablehq.com/@observablehq/plot-barley-trellis.

chart

Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export async function beckerBarley() {
const Plot = await import("https://esm.sh/@observablehq/plot@0.6.13");
const d3 = await import("https://esm.sh/d3@7");
const { document } = await import("https://esm.sh/linkedom@0.15").then((
{ parseHTML: p },
) => p(`<a>`));
const barley = await d3.csv(
"https://raw.githubusercontent.com/observablehq/plot/main/test/data/barley.csv",
d3.autoType,
);
const chart = Plot.plot({
document,
marginLeft: 110,
height: 800,
grid: true,
x: { nice: true },
y: { inset: 5 },
color: { type: "categorical" },
facet: { marginRight: 90 },
marks: [
Plot.frame(),
Plot.dot(barley, {
x: "yield",
y: "variety",
fy: "site",
stroke: "year",
sort: { fy: "x", y: "x", reduce: "median", reverse: true },
}),
],
});
return new Response(
`${chart}`.replace(
/^<svg /,
"<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" ",
),
{ headers: { "Content-Type": "image/svg+xml" } },
);
}

Earthquake map 🌏

This val loads earthquake data from USGS, a topojson file for the land shape, and supporting libraries. It then creates a map and save it as a SVG string. The result is cached for a day. Note that we must strive to keep it under val.town’s limit of 100kB, hence the heavy simplification of the land shape. (For a simpler example, see becker barley.)

Web pagehttps://fil-earthquakes.web.val.run/
Observable Plot https://observablehq.com/plot/
linkedomhttps://github.com/WebReflection/linkedom
topojsonhttps://github.com/topojson/topojson
earthquakeshttps://earthquake.usgs.gov
worldhttps://observablehq.com/@visionscarto/world-atlas-topojson
csshttps://milligram.io/
Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { fetch } from "https://esm.town/v/std/fetch";
import { set } from "https://esm.town/v/std/set?v=11";
let { earthquakes_storage } = await import("https://esm.town/v/fil/earthquakes_storage");
export async function earthquakes(req?) {
const yesterday = new Date(-24 * 3600 * 1000 + +new Date()).toISOString();
if (!(earthquakes_storage?.date > yesterday)) {
const dataUrl =
"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson";
const worldUrl =
"https://unpkg.com/visionscarto-world-atlas@0.1.0/world/110m.json";
let [Plot, { document }, topojson, quakes, world] = await Promise.all([
import("https://esm.sh/@observablehq/plot@0.6.10"),
import("https://esm.sh/linkedom@0.15").then((l) => l.parseHTML("<a>")),
import("https://esm.sh/topojson@3"),
fetch(dataUrl).then((r) => r.json()),
fetch(worldUrl).then((r) => r.json()),
]);
world = topojson.presimplify(world, topojson.sphericalTriangleArea);
world = topojson.simplify(world, 0.0001);
const chart = Plot.plot({
document,
projection: { type: "equal-earth", rotate: [-10, 0] },
r: { type: "linear", domain: [0, 5], range: [0, 10] },
marks: [
Plot.geo(topojson.feature(world, world.objects.land)),
Plot.dot(
quakes.features,
Plot.centroid({
r: (d) => d.properties.mag,
fill: "red",
fillOpacity: 0.3,
}),
),
Plot.graticule(),
Plot.sphere(),
],
});
earthquakes_storage = {
date: new Date().toISOString(),
svg: `${chart}`.replaceAll(/(\.\d)\d+/g, "$1"),
};
await set(
"earthquakes_storage",
earthquakes_storage,
);
}
// If invoked through the web endpoint, return a web page.
return req instanceof Request
? new URL(req.url).searchParams.get("svg")

D3 Chord diagram

Example taken from the D3 Gallery, and rendered (server-side) as a static SVG served through the web end point.

Readme
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { svgServer } from "https://esm.town/v/fil/svgServer";
export async function d3ChordDiagram(req) {
// Import D3 and create a DOM document for server-side-rendering.
const d3 = await import("npm:d3");
const document = await import("https://esm.sh/linkedom@0.15").then((l) =>
l.parseHTML("<a>").document
);
// Data
const data = Object.assign([
[11975, 5871, 8916, 2868],
[1951, 10048, 2060, 6171],
[8010, 16145, 8090, 8045],
[1013, 990, 940, 6907],
], {
names: ["black", "blond", "brown", "red"],
colors: ["#000000", "#ffdd89", "#957244", "#f26223"],
});
// Compute the SVG and return it through the web endpoint
return svgServer(req, chart(data).outerHTML);
//
// ======================================================
//
function chart(data) {
const width = 640;
const height = width;
const outerRadius = Math.min(width, height) * 0.5 - 30;
const innerRadius = outerRadius - 20;
const { names, colors } = data;
const sum = d3.sum(data.flat());
const tickStep = d3.tickStep(0, sum, 100);
const tickStepMajor = d3.tickStep(0, sum, 20);
const formatValue = d3.formatPrefix(",.0", tickStep);
const chord = d3.chord()
.padAngle(20 / innerRadius)
.sortSubgroups(d3.descending);
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
const ribbon = d3.ribbon()
.radius(innerRadius);
const svg = d3.select(document.body).append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
const chords = chord(data);
const group = svg.append("g")
.selectAll()
.data(chords.groups)

Convert a webpage into an ATOM feed, so I can see when there are new activities in the local museum.

feed link: https://fil-musee_angers_activites_feed.web.val.run/

Readme
Fork
1
2
3
4
5
6
7
import { web2atom } from "https://esm.town/v/fil/web2atom";
export let musee_angers_activites_feed = async () =>
web2atom("https://musees.angers.fr/par-date/index.html", {
title: ".tile h3.title",
link: ".tile a",
});

Passerelle RSS vers BlueSky

Ce script tourne une fois par heure et reposte les news de https://rezo.net/ vers le compte https://bsky.app/profile/rezo.net

Il utilise 3 éléments:

  • l'URL du flux RSS
  • une variable de stockage de l'état, qu'il faut créer initialement comme let storage_rss_rezo = {} et qui sera mise à jour par le script
  • les secrets du compte (username et mot de passe de l'application)

Il appelle @me.bsky_rss_poll qui lit le flux, vérifie avec l'état s'il y a du nouveau, et au besoin nettoie le post, puis l'envoie avec le script @me.post_to_bsky. Sans oublier de mettre à jour l'état pour le prochain run.

C'est un premier jet. Merci à @steve.krouse pour val.town et à @jordan pour ses scripts que j'ai bidouillés ici.

À faire éventuellement: améliorer la logique; poster vers twitter.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { set } from "https://esm.town/v/std/set?v=11";
import process from "node:process";
import { storage_rss_rezo } from "https://esm.town/v/fil/storage_rss_rezo";
import { bsky_rss_poll } from "https://esm.town/v/fil/bsky_rss_poll";
export async function cron_rezo_rss2bsky() {
await bsky_rss_poll(
"https://rezo.net/backend/tout",
storage_rss_rezo,
process.env.REZO_BSKY_USERNAME!,
process.env.REZO_BSKY_PASS!,
);
await set("storage_rss_rezo", storage_rss_rezo);
}
Next