xuanjitu is an online, web-based implementation of Xuanji Tu (璇玑图), an ancient palindrome poem by Chinese poet Su Hui. "One of the earliest extant poems by a woman--also among the most complex and unsung--the Xuanji Tu takes the form of a 29 x 29 character grid, embroidered or woven in five colors in silk, written in classical Chinese in the fourth century." --Jen Bervin, in an interview
As the poem can be read horizontally, vertically, or diagonally, in either direction, there are thousands and thousands of ways through the grid that can form rhyming poems of various line lengths. Plenty of scholars have published books and papers about how to read it, over the thousand+ years since it was created, but mostly in the form of text and printed diagrams to show the paths through the grid.
This app animates those readings proposed by scholars, and (eventually, I hope) will allow visitors to discover new readings of their own.
Visit the app here, preferably in Chrome on a device with a large wide screen, like a tablet (landscape mode), laptop, or desktop.
xuanjitu is a Rails app that uses HTML5 Canvas on the frontend.
The technologies used are:
- Backend framework: Rails
- Database: Postgres
- Frontend: JavaScript (TypeScript, ES2020)
- Canvas library: Konva.js
- Rails testing: MiniTest
- JS testing: Jest
- Web hosting and deployment: Heroku
- CI pipeline: CircleCI
The app is still a work in progress (see Roadmap below for future features).
So far, it only has a non-interactive demo mode, where it runs in a continuous loop, about an hour long. The loop consists of over 500 readings (distinct poems) proposed by Michèle Métail in her book Wild Geese Returning: Chinese Reversible Poems (translated by Jody Gladding). Pinyin is provided in the sidebar.
- For all colors other than red, the loop includes every reading listed in Métail's book (with minor exceptions); for red--which has such a disproportionately large number of possible readings that Métail doesn't list them in the book--the loop contains a sampling of readings that would be valid according to Métail's system.
- On pinyin and Classical Chinese: The original poem was composed in Classical Chinese. Here it's rendered using Simplified Chinese characters. Classical Chinese had a different pronunciation system from modern Mandarin, so it doesn't strictly make sense to show the pinyin at all, but I include the modern-day pinyin associated with each character as an aid to understanding.
- More on pinyin: For many characters, there are multiple possible pinyin forms. I have somewhat arbitrarily selected one form per character, but without checking all of them. If you want to make a case for changing any of the pinyin forms shown, do contact me.
A few of the features I plan to tackle next, in order of priority:
- Dictionary definitions: Add the ability to mouse over characters and see some possible definitions.
- A second Xuanji Tu variant: The current implementation is heavily influenced by Métail's interpretation, which I chose because it was available to me in English and easy to follow. There is at least one other interpretation I'd like to implement, which is by Li Wei, a Chinese author. The variant would consist of a different color scheme, a slightly different version of the text, and a different system of rules for forming poems.
- Interactive mode: An alternative to the demo mode, where instead of watching existing readings play, you can create your own, in accordance with existing rules of poem formation.
The Rails models, controllers, and JSON views live in their conventional directories under app/
.
The most important models are:
Position
represents a position within the 29x29 grid, by x- and y-coordinate. This is actually kind of a "behind-the-scenes" model, usually only queried through one of the other models, but it's included here because I think it's helpful for understanding the data model.
Character
represents a Chinese character, with text and form (simplified
vs traditional
). A position has many characters. This is to potentially support both simplified
and traditional
characters at the same position.
Segment
represents a possible valid line (string of characters) in a poem/reading. Segments consist of characters. The same character can belong to multiple segments. So segments and characters are many-to-many. A segment has a head and a tail. All characters in a segment are of the same color (thus, the segment itself has a color
). Segments have different lengths, which vary by which color block they are in and the kinds of line lengths that make sense in a given block.
Reading
represents a possible distinct poem that can be read from the grid, as proposed by a given scholar's interpretation. A reading consists of segments. The same segment can belong to multiple readings. So readings and segments are many-to-many (the join table includes a line_number
to order the segments). To help locate/index it, a reading has a block_number
(I broke the grid into 18 blocks and assigned numbers to them), a number
within its block, and a color
. With these, you can cross-reference each reading between the model and the source text. For example, the reading with { color: "green", block_number: 1, number: 3 }
corresponds to Métail's "Poems in Green", upper left block, 3rd way of reading.
The latter three models are exposed in the API via the endpoints /characters.json
, /segments.json
, /readings.json
, respectively.
The entry point to the frontend code is at app/javascript/packs/index
. The rest of the frontend code lives in app/javascript/lib
. Each module defines a class. Xuanjitu
is the main class and runs the program. The other classes Character
, Segment
, and Reading
each correspond to a backend model.
Almost all of the rules that determine which segments and readings the app should include, exist in the app as config data. They originate in CSVs, some of which were created manually and some of which are generated from Ruby scripts. These CSVs are seeded into the database via the seeds
file.
The CSVs and other source data can be found in db/data
. The generator scripts can be found in db/scripts
.
To change the logic for generating valid segments:
- Edit the script
segments_generator.rb
- Load the file and run
SegmentsGenerator.new.generate
- This will overwrite
db/data/generated_segments.csv
To change the logic for generating readings:
- Edit the script
reading_segment_assignments_generator.rb
(or one of theReadingGenerator
classes it calls on) - Load the file and run
ReadingSegmentAssignmentsGenerator.new.generate
- This will overwrite
db/data/generated_reading_segment_assignments.csv
The pinyin and dictionary definitions come from CC-CEDICT, a downloadable Chinese/English dictionary, converted to CSV form. The full dictionary is not included in the repo. Instead, the script cedict_filter.rb
produces a small dictionary of just the words that are in the poem. This filtered dictionary is used, so far, only in lib/tasks/pinyin_forms.rake
.
To update the filtered dictionary:
- Edit the script
cedict_filter.rb
(note the paths it intends to read from and write to) - Load the file and run
CedictFilter.new.run
- This will overwrite
db/data/filtered_cedict.csv
To change any other source data, you would edit the file manually.
- Clone the repo.
- Dependencies:
bundle
,yarn
- Set up the db:
rails db:create
,rails db:schema:load
(ORrails db:migrate
),rails db:seed
- A couple of rake tasks need to be run after the db is seeded:
rails readings:populate
,rails pinyin_forms:populate
- Run the server:
rails s
, visit onlocalhost:3000
- (Optional) Also run
./bin/webpack-dev-server
for instant TypeScript compilation and hot-swapping. - (Optional) To run tests locally:
rails test
for backend,yarn test
for frontend (or simplyjest
if you havejest-cli
)
If you have any comments, suggestions/requests, corrections, or bug reports, you can open an issue in the repo, or get in touch using this contact form.
This project originated as part of a conversation with artist Jen Bervin, who has been researching and working with Xuanji Tu for years. You can read more about her related project on her own page here, and read an interview with her here (the same one linked to in the Overview).
Readings in the app are from Wild Geese Returning: Chinese Reversible Poems by Michèle Métail, trans. Jody Gladding.