3658 stories
·
3 followers

Search for Charts by Data Visualization Functions

1 Share

What do you want to show?

Here you can find a list of charts categorised by their data visualization functions or by what you want a chart to communicate to an audience. While the allocation of each chart into specific functions isn't a perfect system, it still works as a useful guide for selecting chart based on your analysis or communication needs.


data visualisation

Comparisons

data visualisation

Proportions

data visualisation

Relationships

data visualisation

Hierarchy

data visualisation

Concepts

data visualisation

Location

data visualisation

Part-to-a-whole

data visualisation

Distribution

data visualisation

How things work

data visualisation

Processes & methods

data visualisation

Movement or flow

data visualisation

Patterns

data visualisation

Range

data visualisation

Data over time

data visualisation

Analysing text

data visualisation

Reference tool

data visualisation

Read the whole story
emrox
1 day ago
reply
Hamburg, Germany
Share this story
Delete

Cloud Cost vs. On-Premises Cost

1 Share

Ich wurde gefragt:

Hast Du eine Idee, wie ich seriös die Kosten für Cloud, reguläres und selbst Hosting vergleichen kann?

Seriös nicht, weil das ein Problem mit mehreren Schichten ist und keine dieser Schichten irgendwie schön ist.

Rahmenbedingungen

Der Haupttreiber bei der ganzen Sache ist die simple Tatsache, dass wir immer mehr Transistoren auf einen Chip bekommen. Der Chip wird zwar nicht mehr schneller, weil bei circa 3 GHz Takt Dauerleistung Schluss ist, aber weil die Strukturen immer noch kleiner werden bekommen wir immer mehr Cores und Cache.

Fortschritte bei der Chipentwicklung über Zeit. Die y-Ache ist logarithmisch. Wir sehen, wie die Anzahl der Transistoren pro Chip ungebrochen hoch geht. Circa 2005 haben wir jedoch ein Taktlimit erreicht – bei 3 GHz Dauertakt ist bei Silizium Schluss. Die Performance pro Core geht noch durch Verlagerung gewisser Funktionen in Hardware und effizientere Prozessor-Architekturen langsam weiter hoch. Stattdessen steigt seit 2005 die Anzahl der Cores pro Chip an – die Gewinne an Transistoren gehen also an mehr Cores, nicht in schnellere Cores. Zugleich sinkt der Energieverbrauch pro Core, während der Energieverbrauch pro Chip bei 100-200W stagniert.

Und oft auch Features, die die meiste Zeit gar nicht gebraucht werden, sodass Teile des Chips die meiste Zeit ausgeschaltet sind, um Energie zu sparen. Denn Energieverbrauch erzeugt Abwärme, und die Abwärme setzt dem Chip ein (weiches) Leistungslimit. Wenn wir also Dinge, die viel Energie verbrauchen (die FPU und die Vektoreinheiten) nicht verwenden, dann können wir den Takt bei dem Teil des Chips, der benutzt wird hochdrehen, und bekommen so ein wenig mehr Leistung für viel mehr Energieverbrauch und Abwärme.

Prozessoren von 2024 haben auf einem Chip bis zu 128 oder gar 192 Cores und die doppelte Anzahl von vCores (Threads).

Hardwarekosten

Gehen wir 5 Jahre zurück, kommen wir zu konkreten Kosten.

Die kleinsten sinnvoll kaufbaren Intel Xeon CPUs von 2019 wären die Xeon Silver 4110 gewesen. Auf einer typischen Blade von Dell oder HP gibt uns das 2 Sockets, 16 Cores mit 32 Hyperthreads, 128 GB RAM (8 GB RAM pro Core), eine lokale Micron 1.92 TB NVME mit 800k IOPS, und einen dedizierten 10 GBit/s Port direkt zum Top-of-Rack Switch (luxuriöse 625 MBit/s pro Core).

Was ist das für eine Rechnung?

Ein System muss, damit es funktioniert, balanciert sein. Manche Workloads sind selbst unbalanciert und brauchen exzessiv CPU oder exzessiv Speicher oder Netz. Aber die meisten Enterprise-Anwendungen brauchen pro Core mit 3 GHz 4-8 GB RAM und 100-400 MBit/s Netz. Wenn wir unsere Hardware so proportionieren, dann können wir recht sicher sein, dass sie vielseitig einsetzbar ist.

Preislich kommen wir (die Umgebung, in der ich gearbeitet habe – Eure Preise können variieren) mit solchen schwachbrüstigen Klein-Blades auf Kosten von etwa 120-150 Euro im Monat, für eine geplante Lebensdauer von fünf Jahren. Das ist der Kaufpreis für die Blade, Platz im Rechenzentrum und anteilig Strom, Netzwerk und Blade-Chassis.

Die Blade ist dann noch nicht inventarisiert, noch nicht an, nichts installiert ein Betriebssystem oder die Anwendung – das ist Arbeit, und die rechnen wir hier nicht mit rein.

AWS Kosten im Vergleich

Wir können das mit einer AWS m5.8xl vergleichen: 32 vCPU, 128 GB, 10 GBit/s und kein lokaler Speicher (EBS kostet extra). Die kostet 1232 USD/Monat, also schmale 10x mehr, plus Netzwerk, plus EBS.

Alternativ schauen wir auf eine AWS i3.4xl: 16 vCPU, 122 GB Speicher, und zwei lokale 1.92 TB NVME, sowie ein 10 GBit/s Link. Die kostet 990 USD/Monat, plus Netz, aber wir haben zwei lokale Volumes.

Anders als unsere Blade im Rechenzentrum ist diese VM bereits Teil einer gemanagten Umgebung, wir können sie also provisionieren, und haben sie im Inventory.

Anders als unsere Blade im Rechenzentrum ist diese VM auch eine VM, also ein Teil einer anderen, größeren CPU. Die Silver 4110 ist eine weiche CPU: Unter Last geht ihr Takt runter . Größere CPUs sind je nach Auswahl auch oft härter, sie kommen (zumindest bei Normal Workloads) oft mit viel weniger Taktverlust. Einige Hyperscaler, speziell AWS, lassen bei Intel besondere Typen nach Maß fertigen, die nicht im normalen Portfolio für gewöhnliche Endkunden enthalten sind. Sie haben einen festen Clock, also weder boosten sie, noch gehen sie unter Last runter, und sie brauchen eine speziell gestaltete und genau spezifizierte Kühlung um das leisten. Wenn man als einziger Kunde etwa die Hälfte der gesamten Jahresproduktion Xeons abnimmt bekommt man das.

Ein früherer Arbeitgeber von mir hatte circa 50.000 von solchen Blades. Die meisten davon waren die kleinen 4110, und einige größere Einheiten mit Dual Gold 6132 an drei Standorten. In Summe mag das auf etwa 1.000.000 Cores kommen, und dazu passend ausbalanciert Speicher.

Bei diesem Arbeitgeber hat man um 2010 herum angefangen, Software zu schreiben, um diese Hardware zu verwalten, automatisch zu provisionieren und Betriebssystem-Images und Anwendungen automatisch zu verteilen. Ich kann mir also ein Script schreiben, das 200 Blades vom Typ x in RZ y anfordert. Ich bekomme diese dann binnen 20 Minuten automatisch provisioniert, meiner Team-Abrechnung belastet, und mit Partitionierung, Betriebssystem und Puppet-Klassen laufend zugewiesen. Die ssh-Keys meines Teams erlauben mir einen Zugriff.

In 2019 waren wir soweit, dass kein Rechner eine Uptime von mehr als 90 Tagen hatte. Das heißt, alle Teams hatten ihre Software so verpackt, dass sie vollkommen ohne manuelle Eingriffe installiert werden konnte, keine Single Points of Failures existierten und alle produktiven Maschinen reihum reinstalliert werden konnten. Dadurch haben wir Betriebssystem-Images und Anwendungen laufend neu deployed und aktuell gehalten.

Automatisierung hat natürlich auch vorher schon funktioniert, aber für die Vollautomatisierung war Arbeit und Druck notwendig. Ich habe in This is not a Drill, this is just Tuesday ausführlicher dazu geschreiben.

Jedenfalls war viel Arbeit und es waren viele Leute notwendig, denn ein Produkt, das so etwas für die lokale Firma passend auf Bare Metal Hardware macht, kann man nicht kaufen, und schon gar nicht herstellerunabhängig. Man muss also auf die Kosten für Hardware noch die Kosten für die Entwicklung dieser Stücke Software drauf rechnen.

Am Ende hat sich dieser Arbeitgeber dafür entscheiden, seine Installation in die Cloud zu verschieben, weil Operations dann jemand anderes Problem sind. Das geht natürlich nicht von heute auf morgen, Und soweit ich weiß, betreibt man auch jetzt, 5 Jahre später, noch on-premises und Cloud parallel, und bezahlt beides.

Würde das Projekt abgeschlossen sein, wäre man theoretisch nicht nur das Core.Infrastructure Team los, sondern auch das Staffing für dieses Team. Denn Menschen zu finden, die mit Hardware umgehen können und einen sinnvollen Operations Mindset haben wird zunehmend schwieriger. Entsprechend war das für das Management ein totaler No-Brainer, selbst bei einem Kostenfaktor von 10x.

Der Aufwand, solche Personen zu finden, zu halten und so zu steuern, dass sie dann sinnvolle Automation bauen, ist groß. Das sind nicht nur Geldkosten, sondern schlimmer, das ist auch kognitive Load auf der Organisation.

Es wird nicht einfacher

Allgemeiner gesagt: Damit Du in 2024 eine Firma haben kannst, die eigene Hardware am Start hat, muss Deine Firma erst einmal eine Mindestgröße haben.

Idealerweise kauft man aus Kostengründen immer die zweitgrößte Sorte Rechner, die zu haben ist. Man kann das genau ausrechnen und dann ist es manchmal die zweitgrößte und manchmal die drittgrößte Sorte CPU, die hergestellt wird, bei der der Bumms pro Dollar am besten ist.

Das heißt, man hat dann etwas mit – sagen wir – 128 Cores pro Socket und Single Socket. Bei 8 GB pro Core und 100-300 MBit/s pro Core landen wir bei 128 * 8 GB = 1024 GB = 1 TB RAM und bei 128 * 200 Mbit/s = 25600 MBit/s = 25 GBit/s. Bei einer Dual Socket entsprechend das Doppelte, also 2 TB RAM und 2x 25 GBit/s oder schlicht ein 100 GBit/s Interface (überprovisioniert, aber eventuell einfacher).

Die typische Java-Anwendung kann man in der Regel so bis 8 Cores und 16 GB RAM skalieren, danach haucht die JVM ihr Leben aus. Wir haben also 16-32 Instanzen (Anwendungen) pro Socket. Die meisten werden nicht am oberen Ende der Skala sein, also rechnen wir eher 50-100 Anwendungen pro Socket.

Für viele Firmen ist also eine einzige Maschine schon zu groß. Wir wollen aber aus Gründen der Verfügbarkeit nicht eine Maschine haben, sondern mindestens drei.

Wenn eine solche Maschine aus welchen Gründen umfällt oder neu starten muss, dann sind mal eben 100 Anwendungen offline – wir haben also ein Blast Radius Problem.

Wir können dagegen in der Public Cloud VMs in der passenden Größe kaufen, und weil der Cloud-Anbieter viele solcher Maschinen hat, wird jede unserer VMs “anti-affin” scheduled. Das heißt, der Cloud-Anbieter versucht unsere Anwendungen über so viele Stücke Hardware als möglich zu verteilen, und uns mit so vielen anderen Kunden als möglich auf einer Kiste zu hosten. Wenn dann ein Stück Hardware umfällt, verliert jeder Kunde eine Instanz, aber nicht ein Kunde alle.

On-Premises ist auch nicht unabhängig

Zurück On-Premises: Wir müssen jetzt aus unserer Hardware Stücke in passender Größe schneiden, um sie in Pods oder virtuelle Maschinen zu verwandeln. Für virtuelle Maschinen haben wir die Wahl zwischen VMware und Openstack. Es gibt noch andere Lösungen, aber für die finden wir in der Regel keine Menschen mit Kenntnissen zur Anstellung. Wir haben also die Wahl zwischen lebenslanger Schuldknechtschaft zu Broadcom, oder wir müssen 3 oder mehr Spezialisten einstellen, die dem Openstack hinterher wischen, wenn es mal wieder die Windel voll gemacht hat.

Nun können wir VMs provisionieren, aber wir müssen aus den VMs noch immer Dienste machen. Dort wiederholt sich das Spiel von der VM-Ebene. Am Ende haben wir eine unserer Firmengröße angemesene “Core Infra” Abteilung, die unsere On-Prem Cloud betreibt. Das sind dann vielleicht keine 50.000 Maschinen mit zusammen 1.000.000 Cores mehr, sondern nur noch 8000 Maschinen mit 1.000.000 Cores, aber es sind trotzdem 10-30 Leute, die dafür da sind, darauf Services zu implementieren.

Wir haben also die Wahl, erschreckend kostengünstige Hardware, die viel zu groß für alles ist, zu kaufen. Dann müssen wir Leute finden und anzustellen. Diese sind dann vielleicht in der Lage (oder vielleicht auch nicht), daraus eine brauchbare Plattform zu bauen.

Alternativ können wir dem Bezos Kohle in den Rachen schaufeln.

Die Antwort ist für wirklich jede Firma mit weniger als einer dreistelligen Anzahl von Rechnern absolut offensichtlich, und besteht nicht darin, Hardware zu kaufen. Das Staffing, die lokale Betriebs-Entwicklung und alles, was damit einhergeht, will sich niemand ans Bein binden. Sobald man dann größer ist, ist es teuer, aber zu spät.

Folgerichtig findet man auch immer weniger Leute, die sich mit solchem lokalen Betrieb auskennen, und Dir so etwas bauen können. Dagegen kannst Du Dich mit Leuten tot werfen, die Dir Instanzen bei AWS klicken und für 60k-75k im Jahr zu Dir kommen.

Die ganze Logik-Kaskade

  • Wir haben zu viele Transistoren pro Chip. Die Hersteller von Chips wissen nicht, was die damit tun sollen. Du bekommst absurde Features, die nicht auf allen Chips gebraucht werden (KI-Inferenz, dutzende FPUs und Vektoreinheiten) in den Performance-Cores, oder riesige Mengen Efficiency Cores pro Socket. Im Consumer Bereich bekommst Du statt eines Computers ein System-on-a-Chip, bei dem Grafikbeschleuniger, RAM, RAM-Controller und NVME-Controller mit auf der CPU sitzen, zusammen mit 4E+8P Cores oder ähnlich.
  • Wir haben daher im Enterprise-Bereich viel zu viele Cores pro Socket (128+ Cores).
  • Als Kunde habe ich dadurch das Problem, dass ich kein Bare Metal mehr fahren kann. Ich brauche also K8s oder einen Hypervisor zum Kleinschneiden der Systeme in brauchbare Stücke.
  • Als Kunde habe ich dann auch ein Blast-Radius Problem. Wenn so ein großer Host umfällt, dann nimmt er die ganze Firma offline.
  • Als Kunde will ich also eine VM auf vielen Hosts haben und auf einem Host mit vielen anderen Kunden cohosted werden. Ich gehe also lieber zu einer Public Cloud und miete mir da die passenden VMs.
  • Ich brauche dann auch keine Hardware-Kundigen mehr.
  • Als Hoster von so etwas kann ich die Hardware veredeln und Operations übernehmen, indem ich statt VMs Dienste “as-a-Service” verkaufe.
  • Als Kunde kann ich so nicht nur meine Hardware-Kundigen los werden, sondern für den Dienst x (jetzt xaaS) auch meine x-Kundigen.
  • Als Kunde von so einem Anbieter habe ich nun reine Entwickler, die in komplett abstrakten Layers Dinge zusammenstecken, low-code und nahe an Business Problemen.
  • Dadurch habe ich niemanden nirgendwo mehr, der auch nur einen Hauch einer Ahnung hat, was das alles bedeutet, lastmäßig, und ob der getriebene Aufwand dem Problem angemessen ist.
  • Ich kann solche Einschätzungen aber als Beratung einkaufen, entweder von lokalen Drittanbieter-Consultants oder vom Hoster, als “Well-Architected” Beratung nach Schema F.
  • Das Bruttosozialprodukt steigt, weil das ja jetzt alles Transaktionen mit Echtgeld zwischen Firmen sind, statt Verrechnung mit Spielgeld innerhalb einer Firma.

Und am Ende spielen die Kosten im Vergleich keine Rolle mehr, weil andere, größere Vektoren auf die Firmen einwirken.

Einer dieser Vektoren sind auch immer größere Regulierungsverpflichtungen, die den Betrieb eigener Rechner On-Premises unmöglich machen, weil eine ISO 27k, eine NIS-2 und eine PCI-Zertifizierung ja auch Aufwand und Geld kosten. Kauft man dagegen eine vorab zertifizierte Cloudlösung hat man mindestens drei Viertel des Schmerzes nicht, wird einem versprochen.

Read the whole story
emrox
2 days ago
reply
Hamburg, Germany
Share this story
Delete

You can now download over 33,000 sound effects from the BBC archive

1 Share

The BBC's sound effect library is now stocked up with over 33,000 samples to download for free.

The extensive archive has been open to the public since 2018 but has since doubled in size and, as users pointed out on X, is completely free for anyone to access.

The library's sound effects and recordings date back to the 1920s, and comprise those made in the broadcaster's dedicated studio for use in specific BBC programmes, and those captured as field recordings out in the world. 

Indexed into categories, the sounds span everything from footsteps and transport to nature and machines. Among the plethora of sounds covered are reindeer grunts, rain, clocks, horses walking in mud, common frog calls and crowds at the 1989 FA Cup Final. And that barely scratches the surface. 

The sounds have been released under a non-commercial use license (a RemArc License) as part of the BBC's RemArc programme, which is “designed to help trigger memories in people with dementia using BBC Archive material as stimulation".

A RemArc License stipulates that the samples can only be used for research, educational or personal projects, and therefore can't be legally sampled in music that is then sold. 

Dive into the full archive here and learn more about the history of the BBC sound effect library here. 

Earlier this year, a book exploring field recordings and electronic music was published by DJ Mag's Ben Murphy. 

In other BBC-related news, Tbilisi's underground club scene is set to explored in a new BBC Global Dancefloor documentary while the BBC News theme was recently given a drum & bass remix for journalist Ros Atkins’ Glastonbury DJ set.

Read the whole story
emrox
4 days ago
reply
Hamburg, Germany
Share this story
Delete

OpenFreeMap

1 Share

What is OpenFreeMap?

OpenFreeMap lets you display custom maps on your website and apps for free.

You can either self-host or use our public instance. Everything is open-source, including the full production setup — there’s no ‘open-core’ model here. Check out our GitHub. The map data comes from OpenStreetMap.

Using our public instance is completely free: there are no limits on the number of map views or requests. There’s no registration, no user database, no API keys, and no cookies. We aim to cover the running costs of our public instance through donations.

We also provide weekly full planet downloads both in Btrfs and MBTiles formats.

How can I use it?

Have a look at the default styles and read more about how to integrate it to your website or app:

Quick Start Guide

Who is behind this project and how can I follow it?

I’m Zsolt Ero (blog, email).

After 9 years of running my own map tile infrastructure for MapHub, I’ve open-sourced it and launched OpenFreeMap.

X: @hyperknot (for details)
X: @OpenFreeMapOrg (for announcements)

GitHub: openfreemap and openfreemap-styles

Why did you build this project?

OpenStreetMap is one of the most important collective projects in history. It began 20 years ago, and today, 3 million edits are made each day!

For a long time, when you wanted to use the map on your website or app, you had to look for a commercial map tile provider and hope your site didn’t become too popular. Otherwise, you might end up with a $10,000 bill in a single day, as Hoodmaps did.

Self-hosting was an option, but it required a big server and a lot of time to get it right.

Since I’ve spent many years developing the map tile infrastructure for MapHub, I decided to open-source it so anyone can use it. With OpenFreeMap, you now have the option to either set up your own server or use our public instance.

How can you offer a free public instance ?

There is no technical reason why map hosting costs as much as it does today. Vector tiles are just static files. It’s true that serving hundreds of millions of files is not easy, but at the end of the day, they are just files.

Financially, the plan is to keep renting servers until they cover the bandwidth. I believe it can be self-sustainable if enough people subscribe to the support plans.

If this project helps you save on your map hosting costs, please consider subscribing to a support plan.

If this project helps you save on your map hosting costs, please consider sponsoring me on GitHub Sponsors.

If possible, please choose a monthly donation, even if it’s a smaller amount. The nature of this project needs recurring donations to cover the server costs.

Sponsor me

Is commercial usage allowed?

Yes.

Do you offer support and SLA guarantees?

At the moment, I don’t offer SLA guarantees or personalized support. However, if there’s enough interest, I may introduce a Pro plan in the future. If you’re interested, please let me know by sending an email.

What is the tech stack?

There is no tile server running; only nginx serving a Btrfs image with 300 million hard-linked files. This was my idea; I haven’t read about anyone else doing this in production, but it works really well. (You can read more about it on GitHub.)

There is no cloud, just dedicated servers.

Special thanks go to Michael Barry for developing Planetiler. It made it possible to generate the tiles in 5 hours instead of 5 weeks.

The styles are forked and heavily modified. The map schema is unmodified OpenMapTiles.

Attribution

Attribution is required. If you are using MapLibre, they are automatically added, you have nothing to do.

If you are using alternative clients, or if you are using this in printed media or video, you must add the following attribution:

OpenFreeMap © OpenMapTiles Data from OpenStreetMap

You do not need to display the OpenFreeMap part, but it is nice if you do.

License

The license of this project is MIT. Map data is from OpenStreetMap. The licenses for included projects are listed in LICENSE.md.

Read the whole story
emrox
5 days ago
reply
Hamburg, Germany
Share this story
Delete

Tectonic Surfing

1 Comment and 3 Shares
The worst is when you wipe out in the barrel and you're trapped for several million years until erosion frees you.
Read the whole story
emrox
11 days ago
reply
Hamburg, Germany
Share this story
Delete
1 public comment
cjheinz
18 days ago
reply
LOL! An interesting idea, to think about how different processes would appear, under very different periods of time.
Lexington, KY; Naples, FL
iustinp
17 days ago
I read a nice, short SF story ages ago about this. Can't remember the title, of course.

Font with Built-In Syntax Highlighting

1 Share

Note:

I received a lot of great feedback from the discussions at Mastodon and Hacker News, so I've updated the post with some improvements to the font! I've also added some further examples and acknowledgements at the end.

Syntax Highlighting in Hand-Coded Websites

The problem

I have been trying to identify practical reasons why hand-coding websites with HTML and CSS is so hard (by hand-coding, I mean not relying on frameworks, generators or 3rd party scripts that modify the DOM).

Let's say, I want to make a blog. What are the actual things that prevent me from making—and maintaining—it by hand? What would it take to clear these roadblocks?

There are many, of course, but for a hand-coded programming oriented blog one of these roadblocks is syntax highlighting.

When I display snippets of code, I want to make the code easy to read and understand by highlighting it with colors. To do that, I would normally need to use a complex syntax highlighter library, like Prism or highlight.js. These scripts work by scanning and chopping up the code into small language-specific patterns, then wrapping each part in tags with special styling that creates the highlighted effect, and then injecting the resulting HTML back into the page.

But, I want to write code by hand. I don't want any external scripts to inject things I didn't write myself. Syntax highlighters also add to the overall complexity and bloat of each page, which I'm trying to avoid. I want to keep things as simple as possible.

Leveraging OpenType features to build a simple syntax highlighter inside the font

This lead me to think: could it be possible to build syntax highlighting directly into a font, skipping JavaScript altogether? Could I somehow leverage OpenType features, by creating colored glyphs with the COLR table, and identifying and substituting code syntax with contextual alternates?

<div class="spoilers">
  <strong>Yes, it's possible!</strong>
  <small>...to some extent =)</small>
</div>

The colors in the HTML snippet above comes from within the font itself, the code is plain text, and requires no JavaScript.

To achieve that, I modified an open source font Monaspace Krypton to include colored versions of each character, and then used OpenType contextual alternates to essentially find & replace specific strings of text based on HTML, CSS and JS syntax. The result is a simple syntax highlighter, built-in to the font itself.

If you want to try it yourself, download the font: FontWithASyntaxHighlighter-Regular.woff2

And include the following bits of CSS:

@font-face {
  font-family: 'FontWithASyntaxHighlighter';
  src: 
    url('/FontWithASyntaxHighlighter-Regular.woff2') 
    format('woff2')
  ;
}
code {
  font-family: "FontWithASyntaxHighlighter", monospace;
}

And that's it!

What are the Pros and Cons of this method?

This method opens up some interesting possibilities...

Pros

  1. Install is as easy as using any custom font.
  2. Works without JavaScript.
  3. Works without CSS themes.
  4. ...but can be themed with CSS.
  5. It's fast.
  6. Snippets of code can be put into <pre> and <code>, with no extra classes or <span>s.
  7. Clean HTML source code.
  8. Works everywhere that supports OpenType features, like InDesign.
  9. Doesn't require maintenance or updating.
  10. Works in <textarea> and <input>! Syntax highlighting inside <textarea> has been previously impossible, because textareas and inputs can only contain plain text. This is where the interesting possibilities lie. As a demo, I made this tiny HTML, CSS & JS sandbox, with native undo and redo, in a single, ~200 line web component.

Cons

There are, of course, some limitations to this method. It is not a direct replacement to the more robust syntax highligting libraries, but works well enough for simple needs.

  1. Making modifications to the syntax highligher, like adding more language supports or changing the look of the font, requires modifying the font file. This is inaccessible for most people. I used Glyphs to modify this font, but it only works on Mac, and costs ~300 euros.
  2. It only works where OpenType is supported. Fortunately, that's all major browsers and most modern programs. However, something like PowerPoint doesn't support OpenType.
  3. Finding patterns in text with OpenType contextual alternates is quite basic, and is no match for scripts that use regular expressions. For example, words within <p> tags that are JS keywords will be always highlighted: <p>if I throw this Object through the window, catch it, for else it’ll continue to Infinity & break</p>. Comment blocks can't have new lines etc.

How does it actually work?

Here's roughly how it works. There are two features in OpenType that make this possible: OpenType COLR table and contextual alternates.

OpenType COLR table

OpenType COLR table makes multi-colored fonts possible. There is a good guide on creating a color font using Glyphs.

I made a palette with 8 colors.

I duplicated letters AZ, numbers 09 and the characters . # * - and _ four times. Each duplicated character is then suffixed with .alt, .alt2, .alt3 or .alt4, and then assigned a color from the palette. For example, all .alt1 glyphs are this color.

I also duplicated all characters twice, and gave them suffices .alt1 and .alt5 and assigned them colors used in <!-- comment blocks --> and "strings within quotes"

The two other colors I used for symbols & | $ + − = ~ [] () {} / ; : " @ % and ', and they are always in one color. Numbers 0 1 2 3 4 5 6 7 8 9 are also always a certain color, unless overriden by other rules.

OpenType contextual alternates

The second required feature is OpenType contextual alternates. Here's a great introductory guide to advanced contextual alternates for Glyphs.

Contextual alternates makes characters "aware" of their adjacent characters. An example would be fonts that emulate continuous hand writing, where how a letter connects depends on which letter it connects to. There is a nice article covering possible uses here.

JavaScript syntax rules

The core feature of contextual alternates is substituting glyphs. Here is a simplified code for finding the JavaScript keyword if and substituting the letters i and f with their colored variant:

sub i' f by i.alt2;
sub i.alt2 f' by f.alt2;

In English:

  1. When i is followed by f, substitute the default i with an alternate (i.alt2).
  2. When i.alt2 is followed by f, substitute the default f with an alternate (f.alt2).
  3. As a result, every "if" in text gets substituted with if.

OpenType doesn't support many-to-many substitutions directly, but @behdad on Mastodon had a great suggestion: keywords could be elegantly colored by chaining contextual substitutions.

To do this, I made a lookup which substitutes each letter with its colored variant.

lookup ALT_SUBS {
    sub a by a.alt; 
    sub b by b.alt; 
    sub c by c.alt; 
    [etc.]
    sub Y by Y.alt;
    sub Z by Z.alt;
} ALT_SUBS;

I moved this lookup rule to the Prefix section, which just means it doesn't get applied automatically unlike the other lookups.

Then, I made a lookup rule for each keyword in the contextual alternates section. Here's one for console:

lookup console {
    ignore sub @AllLetters c' o' n' s' o' l' e';
    ignore sub c' o' n' s' o' l' e' @AllLetters;
    sub c' lookup ALT_SUBS
        o' lookup ALT_SUBS
        n' lookup ALT_SUBS
        s' lookup ALT_SUBS
        o' lookup ALT_SUBS
        l' lookup ALT_SUBS
        e' lookup ALT_SUBS;
} console;

First two lines tells it to ignore strings like Xconsole or consoles, but not if there's a period like console.log().

The third line starts by replacing the first letter 'c' with its colored variant c, by using definitions from the other lookup table "ALT_SUBS". This repeats until each letter is replaced by its color variant, and the result is console.

Identifying JavaScript keywords is fairly straightforward. Logic is the same for each keyword, and I used a python script to generate them.

HTML & CSS syntax rules

But for HTML and CSS... I had to get a bit more creative. There are simply too many keywords for both HTML and CSS combined. Making a separate rule for each keyword would inflate the file size.

Instead, I came up with this monstrosity. Here's how I find CSS value functions:

lookup CssParamCalt useExtension {
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' @CssParam parenleft by @CssParamAlt4;
  sub @CssParam' parenleft by @CssParamAlt4;
} CssParamCalt;

@CssParam is a custom OpenType glyph class I've set up. It includes the characters AZ, az, and -, which are all the possible characters used in CSS value function names. Because the longest possible CSS value function name is repeating-linear-gradient(), with 25 letters, the first line of the lookup starts with @CssParam repeated 25 times, followed by parenleft ((). This lookup will match any word up to 25 letters long, if it's immediately followed by an opening parenthesis. When a match occurs, it substitutes the matched text with its alternate color form (@CssParamAlt4).

This lookup works for both CSS and JavaScript. It will colorize standard CSS functions like rgb() as well as custom JavaScript functions like myFunction(). The result is a semi-flexible syntax highlighter that doesn't require complex parsing. I've repeated the same principle for finding HTML tags and attributes, and for CSS selectors and parameters.

Unknown length rules

Comment blocks and strings between quotes also required extra care, because their length can be anything. OpenType doesn't support loops or anything resembling regular expressions. For example, I can't just tell it to simply substitute everything it finds between two quotes.

However, I got a great suggestion from @penteract on Hacker News to use a finite state machine for these kinds of situations. Here our aim is to colorize eveything between /* and */ gray:

lookup CSScomment useExtension {
  // stop if we encounter a colored */
  ignore sub asterisk.alt1 slash.alt1 @All';

  // color first letter after /*
  sub slash asterisk @All' by @AllAlt1;
  sub slash asterisk space @All' by @AllAlt1;
  
  // color /* itself
  sub slash' asterisk by slash.alt1;
  sub slash.alt1 asterisk' by asterisk.alt1;
  
  // finite state machine to color rest of the characters
  // or until ignore condition is met
  sub @AllAlt1 @All' by @AllAlt1;
} CSScomment;

The last line is the important one. The lookup will just continue replacing characters if the previous character is already colored.

End note

The full process is a little bit too convoluted to go into step-by-step, but if you're a type designer who wants to do this with their own font, don't hesitate to contact me. I'm also not an OpenType expert, so I'm sure the substitution logics could be improved upon. I'm open to sharing the modified source file to anyone interested. If you have any ideas, suggestions or feedback, let me know. You can reach me at <a href="mailto:hlotvonen@gmail.com">hlotvonen@gmail.com</a> or leave a comment on Mastodon.

Changing the color theme

You can even change the color theme with CSS override-colors! Browser support is great.

Potential future

Many people suggested that this concept could be taken one step further with harfbuzz-wasm. With harfbuzz-wasm a real parser could be used instead of my crazy opentype lookup rules. Essentially, all the cons could be eliminated... Any harfbuzz-wasm experts who wants to take this on?

Licence

The original font (MonaSpace) has SIL open font license v1.1, which carries over to my modified version. So, you're free to use the font in any way that the SIL v1.1 license permits.

As for the code examples, they are MIT licensed. The tiny sandbox web component can be found here: https://github.com/hlotvonen/tinybox

More examples

as, in, of, if, for, while, finally, var, new, function,
do, return, void, else, break, catch, instanceof, with,
throw, case, default, try, switch, continue, typeof, delete,
let, yield, const, class, get, set, debugger, async, await,
static, import, from, export, extends

true, false, null, undefined, NaN, Infinity

Object, Function, Boolean, Symbol, Math, Date, Number, BigInt, 
String, RegExp, Array, Float32Array, Float64Array, Int8Array, 
Uint8Array, Uint8ClampedArray, Int16Array, Int32Array, Uint16Array, 
Uint32Array, BigInt64Array, BigUint64Array, Set, Map, WeakSet,
WeakMap, ArrayBuffer, SharedArrayBuffer, Atomics, DataView, 
JSON, Promise, Generator, GeneratorFunction, AsyncFunction, 
Reflect, Proxy, Intl, WebAssembly, Error, EvalError, InternalError, 
RangeError, ReferenceError, SyntaxError, TypeError, URIError, 
setInterval, setTimeout, clearInterval, clearTimeout, require, 
exports, eval, isFinite, isNaN, parseFloat, parseInt, decodeURI, 
decodeURIComponent, encodeURI, encodeURIComponent, escape, 
unescape, arguments, this, super, console, window, document, 
localStorage, sessionStorage, module, global

<!-- this is a comment! -->
/* and this */
// and this
<!-- however...
it breaks when your code goes to a newline.
there's no way to keep context line to line...
-->

<!-- can't disable highlighting JS keywords in between tags -->
<p>
  give me a break...
</p>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Syntax Highlighter Example</title>
  <style>
    body {
      background-color: rgb(255, 0, 0);
      font-family: 'Arial Narrow', sans-serif;
      line-height: 1.44;
      color: #333;
    }
  </style>
</head>
<body>
  <header>
    <h1>Welcome to the Syntax Highlighter Test</h1>
  </header>
  <nav>
    <ul>
      <li><a href="#section1">Section 1</a>
    </ul>
  </nav>
  <main>
    <section id="section1">
      <h2>Section 1</h2>
      <p>This is a <span class="highlight">highlighted</span> paragraph.</p>
      <img src="/api/placeholder/300/200" alt="Placeholder image">
    </section>
  </main>
  <script>
    console.log("This is a JavaScript comment");
    function greet(name) {
      return `Hello, ${name}!`;
    }
    document.addEventListener('DOMContentLoaded', () => {
      console.log(greet('Syntax Highlighter'));
    });
  </script>
</body>
</html>

.crazyBackground {
  /* don't try this at home */
  background:
    radial-gradient(
      100% 50% at 50% 50%,
      hsl(90 90% 45%) 0% 5%,
      hsl(250 70% 40%) 50%,
      hsl(50 50% 50%)
    ),
    radial-gradient(
      100% 100% at 50% 25%,
      hsl(90 40% 85%) 30%,
      hsl(40 80% 20%) 60% 90%,
      transparent
    ),
    linear-gradient(
      90deg,
      hsl(150 90% 90%) 0 10%,
      hsl(10 10% 20%),
      hsl(150 90% 90%) 90% 100%
    )
  ;
  background-size:
    5% 10%,
    10% 200%,
    25% 100%
  ;
  background-blend-mode:
    color-dodge,
    difference,
    normal
  ;
  animation: fire2 60s linear infinite;
}

@keyframes fire2 {
  from {
    background-position: 0% 0%, 0 30%, 0 0;
  }

  to {
    background-position: 0% -100%, -100% 30%, 200% 0%;
  }
}

// Variables and constants
let variable = 'Hello';
const CONSTANT = 42;

// Template literals
const name = 'World';
console.log(`${variable}, ${name}!`);

// Function declaration
function greet(name) {
  return `Hello, ${name}!`;
}

// Arrow function
const multiply = (a, b) => a * b;

// Class definition
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    console.log(`Hello, my name is ${this.name}`);
  }
}

// Object literal
const config = {
  apiKey: 'abc123',
  maxRetries: 3,
  timeout: 5000
};

// Array methods
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
const sum = numbers.reduce((acc, curr) => acc + curr, 0);

// Async/await
async function fetchData(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

// Destructuring
const { apiKey, maxRetries } = config;
const [first, second, ...rest] = numbers;

// Spread operator
const newArray = [...numbers, 6, 7, 8];

// Conditional (ternary) operator
const isAdult = age >= 18 ? 'Adult' : 'Minor';

// Switch statement
function getDayName(dayNumber) {
  switch (dayNumber) {
    case 0: return 'Sunday';
    case 1: return 'Monday';
    // ... other cases
    default: return 'Invalid day';
  }
}

// Regular expression
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// Symbol
const uniqueKey = Symbol('description');

// Set and Map
const uniqueNumbers = new Set(numbers);
const userRoles = new Map([['admin', 'full'], ['user', 'limited']]);

// Promises
const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Done!'), 1000);
});

// Export statement
export { greet, Person };

Acknowledgements

Thanks to jfk13 on hn, and @pixelambacht on Mastodon for pointing out that 'calt' is turned on by default, and that 'colr' is not an opentype feature that needs to be "turned on".

Thanks to penteract on hn and @behdad on Mastodon for suggesting better substitution rules.

Thanks to @kizu and @pixelambacht on Mastodon for suggesting color theming with override-colors CSS rule.

As said earlier, if you have any ideas, suggestions or feedback, let me know. You can reach me at <a href="mailto:hlotvonen@gmail.com">hlotvonen@gmail.com</a> or leave a comment on Mastodon.

Thanks to all who sent emails, messages and commented!

Read the whole story
emrox
11 days ago
reply
Hamburg, Germany
Share this story
Delete
Next Page of Stories