-
Notifications
You must be signed in to change notification settings - Fork 0
Example 04 generate a chart
Josh Dionne edited this page Aug 25, 2026
·
1 revision
Generated from the thetowersdk repository. Edits made here are overwritten on the next push — change the source file instead.
/**
* Example 4 — Generate charts from game data.
*
* There is no chart API in this package, and that is the point: a level
* progression already *is* a series. The level is the x-axis and every measured
* field on that level is a line, so producing plot-ready data is a `map`.
*
* Nothing here imports a charting library. The output is `{ x, y }` points you
* hand to Chart.js, D3, Vega, a spreadsheet, or an SVG path you build yourself.
*
* Run it:
* npx tsx examples/04-generate-a-chart.ts
*/
import { LAB_CATALOG, uwStoneChartData } from 'thetowersdk/data'
import { formatNumberForDisplay } from 'thetowersdk/formatting'
type Point = { x: number, y: number }
/** Lab research time is "HH:MM:SS", where hours may exceed 24. */
function durationToHours(raw: string): number {
const parts = raw.split(':').map(Number)
if (parts.length !== 3 || parts.some(Number.isNaN)) return 0
return parts[0] + parts[1] / 60 + parts[2] / 3600
}
// ---------------------------------------------------------------------------
// 1. One entity across its levels — two series from the same progression.
// ---------------------------------------------------------------------------
const lab = LAB_CATALOG.find(entry => entry.name === 'Attack Speed') ?? LAB_CATALOG[0]
const costCurve: Point[] = (lab.levels ?? []).map(level => ({ x: level.level, y: level.cost }))
const timeCurve: Point[] = (lab.levels ?? []).map(level => ({
x: level.level,
y: durationToHours(level.duration),
}))
console.log(`${lab.name}: ${costCurve.length} levels\n`)
console.log(` cost level 1 -> ${formatNumberForDisplay(costCurve[0].y)}`)
console.log(` cost level ${costCurve.at(-1)!.x} -> ${formatNumberForDisplay(costCurve.at(-1)!.y)}`)
console.log(` time level ${timeCurve.at(-1)!.x} -> ${timeCurve.at(-1)!.y.toFixed(1)}h`)
console.log()
// ---------------------------------------------------------------------------
// 2. Many entities at once — the shape most real charts need.
// ---------------------------------------------------------------------------
const allWeapons = Object.values(uwStoneChartData)
const datasets: Array<{ label: string, data: Point[] }> = []
const skipped: string[] = []
for (const weapon of allWeapons) {
const cooldown = weapon.stats?.find(stat => stat.name === 'Cooldown')
// Level 0 costs "Unlock" rather than a number, so it is not plottable.
const data = (cooldown?.levels ?? [])
.filter((level): level is typeof level & { cost: number } => typeof level.cost === 'number')
.map(level => ({ x: level.level, y: level.cost }))
if (data.length === 0) {
skipped.push(weapon.name)
continue
}
datasets.push({ label: weapon.name, data })
}
console.log(`Cooldown stone cost — ${datasets.length} of ${allWeapons.length} ultimate weapons plotted:`)
for (const set of datasets) {
const total = set.data.reduce((sum, point) => sum + point.y, 0)
console.log(` ${set.label.padEnd(22)} ${String(set.data.length).padStart(3)} points ${formatNumberForDisplay(total).padStart(8)} stones total`)
}
// Say what was left out. A filtered-away entity that goes unmentioned reads as
// "this weapon has no cooldown", which is a different claim from "this catalog
// does not model one".
if (skipped.length > 0) {
console.log(`\n no chartable Cooldown series (${skipped.length}): ${skipped.join(', ')}`)
}
console.log()
// ---------------------------------------------------------------------------
// 3. Render it, with no dependencies, to prove the points are all you need.
// ---------------------------------------------------------------------------
/** Lab costs span orders of magnitude, so plot them on a log scale. */
function sparkline(points: Point[], width = 60): string {
const bars = '▁▂▃▄▅▆▇█'
if (points.length < 2) return ''
const step = (points.length - 1) / (width - 1)
const sampled = Array.from({ length: width }, (_, i) => points[Math.round(i * step)].y)
const scaled = sampled.map(y => (y > 0 ? Math.log10(y + 1) : 0))
const max = Math.max(...scaled)
if (max <= 0) return ''
return scaled
.map(value => bars[Math.min(bars.length - 1, Math.floor((value / max) * (bars.length - 1)))])
.join('')
}
console.log(`${lab.name} coin cost (log scale):`)
console.log(` ${sparkline(costCurve)}`)
console.log(`${lab.name} research time (log scale):`)
console.log(` ${sparkline(timeCurve)}`)Generated from TheTowerSDK — do not edit here. Docs and live demos: https://tmrxjd.github.io/TheTowerSDK/
- Quick Start
- Entry Points
- How It Fits Together
- Getting a Save File
- Reading a Save
- Reading The Community Wiki
- Formulas
- Builders
- Charts
- Effective Paths
- Examples
- Templates
- Building a Bot On This
- Google Sheets
- Desktop and Mobile
- Optional Add-ons
- Using It With An AI Agent
- Using Your Own Artwork
- Names And Acronyms
- Patch Notes
- Accuracy
- Versioning
- Where the Docs Live
- Contributing
- Credits
- License
Guides
Examples
- 01-browse-game-data.ts
- 02-read-a-save-file.ts
- 03-plan-upgrades-from-a-save.ts
- 04-generate-a-chart.ts
- 05-generate-a-cost-table.ts
- 06-read-the-community-wiki.ts
- 07-format-like-the-game.ts
- 08-build-a-calculator.ts
- 09-build-a-bot.ts
- 10-read-a-sheet.ts
- 11-build-a-knowledge-base.ts
- 12-show-module-and-card-art.ts
Templates