Sag Chart Creator
Converts sag chart files from various utility software formats into .vlt files compatible with the Vulcan App. Uses JSON structure for improved features and reliability.
How It Works
- Upload — Drop in sag chart files (.txt, .csv, .xlsx, .pdf) or images (.jpg, .png) of stringing charts
- Detect Format — The system auto-detects the source format (Sag10, PLS-CADD, or image/PDF). For images and scanned PDFs, AI vision reads the chart data. Text-based PDFs are read directly.
- Verify — Review every value, edit conductor names, fix any AI errors, reorder sections, and approve each conductor
- Download — Export as .vlt
Supported Input Formats
| Format | File Types | Source |
|---|---|---|
| Sag10 | .txt | Southwire Sag10 software output |
| PLS-CADD | .csv, .xlsx | PLS-CADD stringing reports |
| Image / PDF (AI Vision) | .jpg, .png, .pdf | Photos, screenshots, or scanned PDFs of stringing charts |
AI Vision for Images & PDFs
When sag chart images (JPEG/PNG) or scanned PDFs are uploaded, the system uses AI vision (Claude by Anthropic) to extract the data. Text-based PDFs are read directly without AI. This is highly effective but not perfect:
- Images and PDFs are processed via the Anthropic API (encrypted in transit, not used for training)
- Accuracy is typically 99%+ on clean, high-resolution charts
- All extracted values must be reviewed on the verification screen
- Individual cells can be edited directly if the AI misreads a value
JSON Schema (Version 3)
Nests the chart as Section → Conductor → Condition → Phase → Ruling Span → span. The structure is always the same shape: containers are always present, labels are optional. A chart with no section grouping is one section without a name; a chart that doesn't break out phases is one phase entry without a phase label. Optional keys are omitted entirely when empty.
{
"version": 3, // Required. Schema version.
"title": "Chart Name", // Required. Display name.
"createdBy": "Company/Person", // Optional. Author.
"createdDate": "2026-07-22", // Optional. Date string.
"loadingDistrict": "250B", // Optional. Loading district.
"settings": { // Required. Chart-level config.
"units": { // Required. Unit for every value below.
"rulingSpan": "ft"|"m",
"spanLength": "ft"|"m",
"temperature": "F"|"C",
"tension": "lbf"|"kN"|"N"|"kgf",
"sag": "ft"|"m"|"in"|"cm",
"weight": "lb/1000ft"|"lb/ft"|"kg/m"|"kg/km"
},
"tolerances": { // Optional. File-wide default, auto-applied on load.
"tension": { // Optional. If present, plus and minus both required.
"plus": 50, // Max above target.
"minus": 30 // Max below target.
},
"sag": { // Optional. If present, plus and minus both required.
"plus": 0.5, // Max above target.
"minus": 0.5 // Max below target.
}
}
},
"sections": [ // Required. At least one.
{
"name": "1A to 3", // Optional. Pulling section header.
"conductors": [ // Required. At least one.
{
"name": "795 26/7 ACSR Drake", // Required. Display name.
"weight": 1094, // Optional. In settings.units.weight.
"tolerances": { // Optional. Overrides settings.tolerances for this conductor.
"tension": {
"plus": 40,
"minus": 25
},
"sag": {
"plus": 0.25,
"minus": 0.25
}
},
"conditions": [ // Required. 1..N, ordered. Free-form
{ // names (Initial/Final/Creep/...).
"name": "Initial", // Required. Condition name.
"phases": [ // Required. At least one.
{
"phase": "A", // Optional. Phase label.
"rulingSpans": [ // Required. At least one.
{
"value": 500, // Required. Ruling span length.
"spanLengths": [300, 400, 500, 600], // Required. One or more spans.
"spanTitles": ["1A-2", "2-3", "3-4", "4-5A"], // Optional. Matches spanLengths count.
"data": [ // Required. Ascending temp order.
{
"temperature": 0, // Scalar. In settings.units.temperature.
"tension": 5100, // Scalar. In settings.units.tension.
"sags": [1.42, 2.53, 3.95, 5.69] // One value per spanLength.
},
{
"temperature": 60,
"tension": 4670,
"sags": [1.75, 3.11, 4.87, 7.01]
},
{
"temperature": 120,
"tension": 4210,
"sags": [2.19, 3.90, 6.09, 8.77]
}
]
}
]
},
{
"phase": "B", // Each phase: its own spanLengths/data.
"rulingSpans": [ ... ]
}
]
},
{
"name": "Final", // Another condition, same shape.
"phases": [ ... ]
}
// ...and any others (e.g. "Creep") in the same form.
]
}
]
}
]
}
Schema Notes
- Units are required (v3)
settings.unitsis required and declares the unit of every numeric value in the file. Imperial values are ft, °F, lbf, and lb/1000ft. The app converts to internal units on import and handles display conversion. (Legacy version 2 files may omit units and default to imperial.)- Tolerance sides are paired
- A
tolerancesblock is optional, and each metric (tension,sag) within it is optional — but when a metric is present, bothplusandminusare required. - Array counts must match
- Each
sagsarray must have the same number of entries asspanLengthsfor that ruling span. IfspanTitlesis present, it must also match. - Temperature rows in ascending order
- Data rows should be sorted by temperature (low to high) for interpolation to work correctly.
- Conditions are an arbitrary named list
- A conductor's
conditionsarray holds one or more independent conditions, each with a free-formname— not limited to Initial/Final. Any label (Creep,Everyday,After Load) is preserved verbatim, and each condition carries its ownphases/rulingSpans/data(never interleaved columns). Order is Initial, then Final, then any others. - One file, many conductors
- A single .vlt file can contain multiple conductors, each with multiple conditions and ruling spans.
- One shape, always (v3)
- The nesting
sections → conductors → conditions → phases → rulingSpansis identical in every file — containers are never skipped or flattened. What varies is the optional labels: a chart with no section grouping is one section withoutname; an unphased chart is one phase entry withoutphase. A reader is a single loop with no shape branching. - Blank optional keys are omitted (v3)
- Optional keys are left out entirely when they have no value — never written as
null. No section grouping → noname; unphased → nophaselabel; no structure numbers → nospanTitles. - Per-conductor tolerances (v3)
settings.tolerancesis the file-wide default. A conductor may optionally carry its owntolerancesobject (same asymmetricplus/minusshape); when present it replaces the file default entirely for that conductor — no per-metric merging. A reader should check the conductor first, then fall back tosettings.tolerances.- Phase sits above ruling span (v3)
- Because the three phases hang slightly differently, each phase's ruling span can differ. Each
phases[]entry therefore carries its ownrulingSpans, and each of those its ownvalue,spanLengths,spanTitles, anddata. - Reader back-compatibility (v3)
- Version 2 files use the top-level
conductorsarray; version 3 usessections. A reader should branch onversion: walksectionswhen it is 3 or higher, otherwise the existingconductorspath.
Legacy Schema (Version 2)
Flat conductor list — no sections or phases. Still read by the app. Each sag chart file contains one or more conductors, each with conditions, ruling spans, and temperature data rows:
{
"version": 2, // Required. Schema version.
"title": "Chart Name", // Optional. Display name.
"createdBy": "Company/Person", // Optional. Author.
"createdDate": "2026-03-30", // Optional. Date string.
"loadingDistrict": "250B", // Optional. Loading district.
"settings": { // Optional. Chart-level config.
"units": { // Optional. Defaults to imperial.
"rulingSpan": "ft"|"m",
"spanLength": "ft"|"m",
"temperature": "F"|"C",
"tension": "lbf"|"kN"|"kgf",
"sag": "ft"|"m"|"in"|"cm",
"weight": "lb/1000ft"|"lb/ft"|"kg/m"|"kg/km"
},
"tolerances": { // Optional. Auto-applied on load.
"tension": { // Optional. Asymmetric per direction.
"plus": 50, // Max above target. Optional.
"minus": 30 // Max below target. Optional.
},
"sag": { // Optional. Asymmetric per direction.
"plus": 0.5, // Max above target. Optional.
"minus": 0.5 // Max below target. Optional.
}
}
},
"conductors": [ // Required. At least one.
{
"name": "336.4 26/7 ACSR", // Required. Display name.
"weight": 462, // Optional. In settings.units.weight.
"conditions": [ // Required. At least one.
{
"name": "Initial", // Required. Condition name.
"rulingSpans": [ // Required. At least one.
{
"value": 500, // Required. Ruling span length.
"spanLengths": [300, 400, 500, 600], // Required. Span lengths.
"spanTitles": ["1-2", "2-3", "3-4", "4-5"], // Optional. Must match spanLengths count.
"data": [ // Required. Rows in ascending temp order.
{
"temperature": 60, // In settings.units.temperature.
"tension": 4670, // In settings.units.tension.
"sags": [1.75, 3.11, 4.87, 7.01] // In settings.units.sag. Must match spanLengths count.
}
]
}
]
}
]
}
]
}
Need the legacy CSV format? CSV Converter