A bar is an Entry
Shipped. The Segment type no longer exists — the word does. A Segment is a Bar on a row that draws more than one Bar, and nothing stores one. A bar is an ordinary child Entry, described in
CONTEXT.md.
The claim, in one line
Every bar on the timeline is one Entry. A normal bar, one of several bars sharing a row, and a summary rail are the same authored shape. Two questions decide which one a reader sees, and neither one is stored on the Entry:
- Does it have children? That decides derivation and the default look today.
- Does a rule on the row source match its parent? That decides whether it gets a row of its own, or draws on its parent's row. This is the one new question. The rule reads one parent Entry at a time, so the same key answers "every parent", "the parents this Field marks" and "this one parent, right now".
A Segment becomes a regular Entry with parentId set. Nothing on the child marks it.
Three bars, one shape
The API, in call sites
Every job below already ships, except one row.
const entries = [
{ id: 'req-1', name: 'Framing crew', showDaysOnRow: true }, // the row
{ id: 'd1', parentId: 'req-1', start, end, hours: 8 }, // a bar: a plain Entry
{ id: 'd2', parentId: 'req-1', start, end, hours: 4, filled: true },
{ id: 'hold', name: 'Site hold', start, end }, // a plain row, as today
];
const dataset = new Dataset<Props>({
// Core ships `name`, `start`, `end` and `duration`. Every Field below is this consumer's own.
fields: [
{ key: 'showDaysOnRow', type: 'boolean' }, // the marker the row rule reads
{ key: 'hours', type: 'number', rollUp: 'sum' },
{ key: 'filled', type: 'boolean' },
],
entries,
});
new Gantt({
container,
dataset,
rowSource: { source: 'entries', childrenAsSegments: { showDaysOnRow: true } },
});
This dataset is two levels deep and draws two rows: req-1, carrying d1 and d2 as bars, and
hold. It names no tree, because no row nests under another one here. tree is a separate
question, and the next section answers it.
| Job | Call site | New? |
|---|---|---|
| Author a bar with data | { id: 'd1', parentId: 'req-1', start, end, hours: 8 } | no |
| Draw every parent's children on its row | rowSource: { …, childrenAsSegments: true } | the one new key |
| Draw one parent's children on its row | rowSource: { …, childrenAsSegments: { showDaysOnRow: true } }, and mark that parent | the same key |
| Open one row into sub-rows, live | dataset.entries.update('req-1', { showDaysOnRow: false }) | no |
| Read a bar's value | entry.read('hours') | no |
| Name the row a bar sits on | entry.parent() | no |
| List a row's bars | entry.children() | no |
| Total bar values onto the row | { key: 'hours', type: 'number', rollUp: 'sum' } | no |
| Patch one bar | dataset.entries.update('d2', { hours: 6 }) | no |
| Add one bar | dataset.entries.add({ parentId: 'req-1', start, end, hours: 8 }) | no |
| Remove a bar | dataset.entries.remove('d1') | no — several bars are several calls in one transaction |
| Move a bar to another row | dataset.entries.update('d1', { parentId: 'req-2' }) | no |
| Give a bar its own look | variants: [{ name: 'fullDay', when: { hours: 8 }, paint, css }] | no |
| Gate a gesture for every bar | capabilities: { resize: (entry) => entry.read('filled') !== true } | no |
| Gate a gesture for one look's bars | variants: [{ name: 'fullDay', when: { hours: 8 }, can: { resize: false } }] | no |
| Turn off drag to another row, in both panes | capabilities: { reorder: false } | no |
| Read the change | { store: 'entries', id: 'd2', field: 'hours', from: 4, to: 6 } | no |
| Propose a cascade | an EditExtender returns EntryEdits | no |
| Show the same bars as sub-rows | change the rule, or the value it matches | no |
Two doors gate a gesture, and they do not compete. A bar's capability resolves down one chain:
the consumer's own capabilities, then the resolved Variant's capabilities, then the library rule.
Both doors take the same Capabilities shape, and a predicate at either level answers undefined
for "no opinion", which falls to the next level. Write can when the rule belongs to the look — a
milestone never resizes, wherever it is drawn. Write capabilities when the rule belongs to this
Gantt — a read-only board resizes nothing, whatever a row looks like.
Set it for the whole Gantt, or for one Entry
One key answers both. childrenAsSegments takes true for every parent, or an EntryRule
(src/layout/entry-rule.ts) — the same when pattern a variant takes. The rule runs once per
parent Entry in the layout pass, so the scope of the setting is whatever the rule says:
const forms: readonly (EntryRule | true)[] = [
true, // every parent
{ team: 'framing' }, // any value the data already holds
{ showDaysOnRow: true }, // the parents the consumer marks
(entry) => entry.children().length > 3, // whatever a predicate can ask
];
The common case is the shorthand and the long form is the expert one, as every other config key on
this library reads. true fits a dataset two levels deep, where every parent carries bars. Name the
level with a match or a predicate when the dataset is deeper.
A field match names a Field and a value, and nothing else. { showDaysOnRow: true } matches every
parent whose showDaysOnRow Field equals true. { team: 'framing' } matches every parent whose
team Field equals 'framing'. The key is a Field key the Dataset declares — a core one, or the
consumer's own — and the comparison is that Field's own equals. A key no Field declares matches
nothing, and the miss reports once per rule and key on the error event
(code: 'unknown-row-source-field', severity: 'warning', with a console.warn fallback when
nothing subscribes) — the frame keeps drawing, and the typo is loud rather than silent.
unknown-variant-field is a different code, for variants' own when. A match is equality, never "has
a value": ask that with a predicate.
It does not name a variant, and it does not pick one. variants writes the same shape in its
when, so an author learns one match syntax — but the two answer different questions. when asks
how does this row look; childrenAsSegments asks does this parent give its children rows. A
segmented parent's children each still resolve their own variant afterwards, and that is the per-bar
name, look and capability this design enables.
tree is a separate question
tree says whether a non-segmented parent's children nest under it. The new rule says whether a
segmented parent's children become rows at all. Neither reads the other, and the fold runs in both
the flat and the tree branch of resolveEntriesSource:
tree | childrenAsSegments | What a reader sees |
|---|---|---|
false (default) | none | every Entry is a row, flat — today's grid |
false | matches req-1 | req-1 is a row with two bars; d1/d2 have no rows. Still a flat grid |
true | none | d1/d2 nest under req-1 as sub-rows, with a chevron — today's treegrid |
true | matches req-1 | req-1 holds the bars; a parent the rule does not match still nests |
So the two-level roster names no tree, and gets no tree chrome for rows that do not nest. A dataset
with a grouping level above the segmented parents names it, and the next section is that case.
A summary row above, segmented rows below
A segmented row is an ordinary row, so an ordinary parent may sit above it. The rule matches the
parents that carry bars; their own parent matches no rule, keeps its row, wears core's summary() and
rolls up as it always has:
const deeper = [
{ id: 'site-a', name: 'Site A' }, // a summary row
{ id: 'req-1', parentId: 'site-a', name: 'Framing crew', showDaysOnRow: true },
{ id: 'req-2', parentId: 'site-a', name: 'Roofing crew', showDaysOnRow: true },
{ id: 'd1', parentId: 'req-1', start, end, hours: 8 }, // a bar on req-1's row
{ id: 'd2', parentId: 'req-1', start, end, hours: 4 },
];
const rowSource: EntriesRowSource = {
source: 'entries',
tree: true,
childrenAsSegments: { showDaysOnRow: true },
};
Three rows: site-a with a summary rail over everything below it, then req-1 and req-2, each
carrying its own days as bars. site-a collapses and expands through the chevron, as a parent of
rows does today. The Rollup runs over the whole tree in one bottom-up pass, so site-a's hours
cell totals every day under both crews. No stage of the pass asks whether a row carries bars of its
own children.
A segmented row is a summary in the grid already. The segmented parent's cells roll up from its
children — req-1 reads 12 h with d1 and d2 on its row. The one thing the design
suppresses is the parent's own bar, so summary()'s rail does not paint over the children it
stands for. Core ships nothing to put one back: no rail key, no rail concept, no helper. A consumer who wants a band behind the bars writes one variant with a producer of their own, and that producer ignores the third parameter rather than reading it:
const bars: BarProducer = (entry, variant) => [wholeEntryBar(entry, variant)];
This producer always draws, segmented row or not — it is what a rail actually wants. wholeSpanUnlessSegments, the parameter it ignores, is core's own answer of when to suppress; a consumer producer that wants its band to survive claiming skips that question and always paints, so it always sits behind the children's bars rather than disappearing the moment childrenAsSegments matches the row.
:::note Terminology in this page
The library retired Item in favour of Bar, so this page writes Bar, bars and
wholeSpanUnlessSegments. MenuItem and CellItem keep the word "item" for menu rows and cells.
:::
:::note Why the key says "segments"
Read the call site aloud: "row source: entries, children as segments, where show-days-on-row is
true." The name says what the children become, and it discriminates — an parent no rule matches's
children draw a bar on a row of their own, and never a segment of another row's bar. The key carries
no Row, because it already sits on rowSource and a name does not repeat its own context.
The word is free because the type is gone. Segment stopped being a stored type and
comes back in the glossary with one meaning and nothing behind it: a child Entry drawn as one piece
of its parent's row. Rejected: childrenAsRowSegments, childrenOnParentRow, childrenAsBars
(a parent no rule matches has children that draw bars too), mergeChildRows (the mechanism, not the job) and
splitRow ("Split" is under Avoid in CONTEXT.md).
:::
How it flows through the layout pass
What each stage answers, per case
| Stage | A normal bar | A segment bar | A summary bar |
|---|---|---|---|
resolveRows | one row, entryIds: ['t1'] | one row, entryIds: ['req-1','d1','d2']; children get none | one row each; children nest at depth + 1 |
| Row is expandable | no | no — the rule opens it, not a chevron | yes |
resolveFor(entry) | bar(), the last resort | per child: whatever rule matches it | summary() matches on entry.hasChildren |
variant.bars(entry) | one Bar over [start, end) | one Bar per child Entry; the parent itself produces none | one rail Bar (wholeSpanUnlessSegments) |
| Rollup writes | nothing | the parent's hours, start, end | the same three, identically |
placeFrame | one FrameBar | one FrameBar per Bar | one rail FrameBar |
| Selection unit | the Entry | the Entry | the Entry |
How a bar gets updated
How a bar moves to another row
What each layer sees
| Layer | What changes |
|---|---|
model/ | Segment, StoredSegment, SegmentId, SegmentInput, SegmentEdit and the four Segment errors are deleted. Nothing replaces them |
data/ | updateSegment, addSegment, removeSegments and the store: 'segments' apply path go. The Rollup already gives a parent its children's values, and that is now also the envelope |
layout/ | One new key on EntriesRowSource, one fold in resolveEntriesSource, and a segmented parent draws no Bar of its own. followSegments is deleted; ignoreSegments becomes wholeSpanUnlessSegments |
render/ | FrameRow.segmentIds, FrameBar.segmentIds and Bar.segmentId go. A bar keys on its BarId and names its EntryId, as it did before Segments |
view/ + interaction/ | selectedSegmentIds, segmentIdsForBar, segmentIdsForRow and the segmentIds half of DomTarget and CommandTarget go. The Selection holds Entry ids |
scheduling/ | Unaffected by this page. A link to a split piece of work names the parent or one child, and the scheduling plugin rules that |
What it costs
| Cost | Where it stands |
|---|---|
| Performance | Measured, and the objection falls. 10,000 bars build a frame 20% cheaper as child Entries than as Segments. One write and one row resolution grew, and each halves once the Rollup stops re-deriving an index the store already memoizes. A browser measurement of the hover path is still owed |
| A parent's own bar | Core draws no bar for a segmented parent. A consumer variant may still paint a rail. |
| The Selection unit | The Entry. 16 non-test files that named segmentIds now use EntryIds instead. |
| A shared row's nine call sites | entryIds[0] is read nine times in seven files. Seven mean "the row's subject", and a segmented row holds N+1 ids. Two mean "the first selected Entry", and a segmented row can select several. Each one is read once and fixed. |
| Mixed children | The rule matches the parent, so all of that parent's children draw as Segments. A parent with days on its row and sub-tasks below cannot be expressed. No consumer has asked for it |
| A segment child with children of its own | A segment rule is a collapse, one level deeper. The segmented parent draws its direct children as bars; the subtree below loses its rows, and a child that derives draws one rolled-up bar — what a collapsed parent's bar does today |
| Migration | This library has never shipped to a user, so the segments key is deleted outright. 59 non-test sites and 37 test files that read it have been updated. |
| Copying a row with its bars | toInput() copies one Entry. A subtree copy needs its own door — with or without this design |
Related
- Row source updates — change one row-source setting and keep the rest.