Gartner hype cycle graph
disl0.50.5Gartner hype cycle graph
Example definition gartner-hype-cycle.dis: A trimmed excerpt of the Gartner hype cycle graph definition (definitions/diagrams/gartner-hype-cycle-graph.dis), used to show DISL 0.2 and 0.3 constructs.
Views generated from the file
These views are drawn by this site from gartner-hype-cycle.dis itself: its metamodel, and the layers it fills. They are not the notation the definition draws; that appears in an IDE that runs it.
gartner-hype-cycle.dis at 4e69c28Diagram as text (Mermaid)
classDiagram
accTitle: Metamodel of the Gartner hype cycle graph example
accDescr: Class diagram of gartner-hype-cycle.dis: 3 node types (Trend, Trigger, Note) and 1 relation types (Influence), with their typed attributes, inheritance, containment and relation ends.
class Trend {
+string name
+text description
+string[] tags
+yearMonth start
+yearMonth stop
+int row
+int phases
+yearMonth peakEnd
+yearMonth troughEnd
+yearMonth slopeEnd
}
class Trigger {
+string name
+yearMonth date
+int row
+string[] tags
+text description
+datetime announcedAt
+datetime shippedAt
}
class Note {
+text text
+yearMonth at
+int row
}
class Influence {
<<relation>>
+Phase fromPhase
+Edge fromEdge
+number fromAt
+Phase toPhase
+Edge toEdge
+number toAt
+text description
}
Influence --> Trend : source
Influence --> Trigger : source
Influence --> Trend : targetgartner-hype-cycle.dis at 4e69c28Diagram as text (Mermaid)
flowchart LR
accTitle: Layers of gartner-hype-cycle.dis
accDescr: The eight DISL layers in order. gartner-hype-cycle.dis fills 8 of them: Metamodel (8 items), Coordinates (6 items), Notation (15 items), Toolbox (6 items), Constraints (19 items), Behavior (4 items), Layout (4 items), Persistence (6 items).
L1["1 · Metamodel<br/>8 items"]
L2["2 · Coordinate systems, placement and snapping<br/>6 items"]
L3["3 · Notation (visual definition)<br/>15 items"]
L4["4 · Toolbox, context tools and forms<br/>6 items"]
L5["5 · Constraints<br/>19 items"]
L6["6 · Behavior<br/>4 items"]
L7["7 · Layout<br/>4 items"]
L8["8 · Persistence<br/>6 items"]
L1 --> L2 --> L3 --> L4 --> L5 --> L6 --> L7 --> L8
classDef filled stroke-width:3px
classDef empty stroke-dasharray:4 4
class L1 filled
class L2 filled
class L3 filled
class L4 filled
class L5 filled
class L6 filled
class L7 filled
class L8 filled- Layer 1 — Metamodel: 8 items
- Layer 2 — Coordinate systems, placement and snapping: 6 items
- Layer 3 — Notation (visual definition): 15 items
- Layer 4 — Toolbox, context tools and forms: 6 items
- Layer 5 — Constraints: 19 items
- Layer 6 — Behavior: 4 items
- Layer 7 — Layout: 4 items
- Layer 8 — Persistence: 6 items
The file
Download gartner-hype-cycle.dis (36,545 bytes, unchanged from the source).
{
"$schema": "https://etalii.net/adp/disl/schema/0.2/disl.schema.json#/$defs/Specification",
"disl": "0.3",
"language": {
"id": "org.example.gartner.hype-cycle",
"version": "0.1.0",
"origin": "gartner/hypecycle-graph",
"label": "Gartner hype cycle graph",
"requires": { "conformance": "standard", "features": ["axis.yearMonth", "ruler.adaptive", "snap.byGesture", "anchor.part", "canvas.filters", "viewpoint.variants", "bounds.neighbour", "ruler.minUnit", "shape.partTooltip", "anchor.drawnFrom", "label.editText", "form.parse", "layout.rowPacked", "layout.rows"] },
"doc": {
"summary": "A trimmed excerpt of the Gartner hype cycle graph definition (definitions/diagrams/gartner-hype-cycle-graph.dis), used to show DISL 0.2 and 0.3 constructs.",
"description": "Trends drawn as phased banners over time, one row each, with the triggers that started them and the influences between them. The excerpt keeps the types, a simplified phased banner, the axes, the toolbox, the forms and the rules; the exact banner geometry, the `.ghg` file format and the layout plugin of the full definition are left out. Dates are `yearMonth` values, so the step-conversion functions of the full definition are gone. It shows the month-index time axis with a bound unit, multi-level and adaptive rulers, snapping per gesture, boundary handles that write the model, influence ends bound to phase parts, the tag filter, the compact toggle and several stand-ins for timeline and wardley-map. From DISL 0.3 it shows phase boundaries kept a month apart, phase colours from theme tokens, a ruler that never goes finer than the diagram's unit, phase tooltips, a trigger label that edits only the name, trigger anchors drawn from the outline, a Phases field that shows and accepts its own text, a code per case of `std.endpoints`, the row-packed compact layout and the rows-only Arrange."
}
},
"functions": {
"visiblePhases": {
"params": [{ "name": "t", "type": "Element" }],
"returns": "int",
"cel": "min(max(t.phases, 1), 4)"
},
"boundaryFraction": {
"params": [{ "name": "t", "type": "Element" }, { "name": "i", "type": "int" }],
"returns": "double",
"cel": "cel.bind(b, i == 1 ? t.?peakEnd : (i == 2 ? t.?troughEnd : t.?slopeEnd), b.hasValue() && t.stop > t.start ? double(b.value() - t.start) / double(t.stop - t.start) : double(i) / double(visiblePhases(t)))",
"doc": "Where boundary `i` lies as a fraction of the banner: the stored boundary, or an even spread."
},
"fractionToMonth": {
"params": [{ "name": "t", "type": "Element" }, { "name": "f", "type": "double" }],
"returns": "int",
"cel": "t.start + int(math.round(f * double(t.stop - t.start)))",
"doc": "The month index a fraction of the banner falls on; `yearMonth` values are month indices in CEL."
},
"formatWhen": {
"params": [{ "name": "m", "type": "yearMonth" }, { "name": "u", "type": "string" }, { "name": "long", "type": "bool" }],
"returns": "string",
"cel": "u != 'month' ? string(m.year()) : (long ? ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])[m.month() - 1] + ' ' + string(m.year())",
"doc": "A date as a trigger shows it: `Dec 1947` (or `December 1947` when long) in a diagram of months, the year alone in one of years or coarser."
},
"phaseName": {
"params": [{ "name": "i", "type": "int" }],
"returns": "string",
"cel": "['peak', 'trough', 'slope', 'plateau'][clamp(i, 0, 3)]"
}
},
"metamodel": {
"diagram": {
"attributes": {
"unit": { "type": "TimeUnit", "default": "month", "doc": "The step one unit of the time axis stands for, and the snapping unit." }
}
},
"enums": {
"TimeUnit": {
"ordered": true,
"values": {
"month": { "label": "Month" },
"year": { "label": "Year" },
"decade": { "label": "Decade" },
"century": { "label": "Century" },
"millennium": { "label": "Millennium" }
},
"doc": "Unit names as DISL's time units spell them, so the axis scale and the snap rule can bind to the diagram's `unit`."
},
"Phase": {
"ordered": true,
"values": {
"peak": { "label": "Peak", "color": { "token": "ghg.peak" } },
"trough": { "label": "Trough", "color": { "token": "ghg.trough" } },
"slope": { "label": "Slope", "color": { "token": "ghg.slope" } },
"plateau": { "label": "Plateau", "color": { "token": "ghg.plateau" } }
}
},
"Edge": {
"values": { "top": { "label": "Top" }, "bottom": { "label": "Bottom" } }
}
},
"types": {
"Trend": {
"label": "Trend",
"labelAttribute": "name",
"attributes": {
"name": { "type": "string", "required": true, "group": "Identity", "order": 1 },
"description": { "type": "text", "group": "Identity", "order": 2 },
"tags": { "type": "string", "many": true, "group": "Identity", "order": 3, "doc": "Filtered on, case-insensitively." },
"start": { "type": "yearMonth", "required": true, "group": "Time", "order": 1, "min": "-3200-01" },
"stop": {
"type": "yearMonth",
"required": true,
"group": "Time",
"order": 2,
"min": { "cel": "self.start + 1" },
"outOfRange": { "drag": "clamp", "typed": "refuse", "message": { "cel": "'A trend is at least a month long; it cannot stop at ' + formatYearMonth(newValue, 'u-MM') + ' when it starts at ' + formatYearMonth(self.start, 'u-MM') + '.'" } }
},
"row": { "type": "int", "default": 0 },
"phases": { "type": "int", "default": 4, "min": 1, "max": 4, "group": "Phases", "order": 1 },
"peakEnd": {
"type": "yearMonth",
"group": "Phases",
"order": 2,
"min": { "cel": "self.start + 1" },
"max": { "cel": "self.stop - 1" },
"outOfRange": { "drag": "clamp", "typed": "refuse", "message": { "cel": "'A phase boundary lies inside the trend, between ' + formatYearMonth(min, 'u-MM') + ' and ' + formatYearMonth(max, 'u-MM') + '.'" } },
"doc": "Where Peak ends, once dragged. Absent means spread evenly. A boundary drag stops at the bound; a typed month past it is refused."
},
"troughEnd": { "type": "yearMonth", "group": "Phases", "order": 3, "min": { "cel": "self.start + 1" }, "max": { "cel": "self.stop - 1" }, "outOfRange": { "drag": "clamp", "typed": "refuse" } },
"slopeEnd": { "type": "yearMonth", "group": "Phases", "order": 4, "min": { "cel": "self.start + 1" }, "max": { "cel": "self.stop - 1" }, "outOfRange": { "drag": "clamp", "typed": "refuse" } }
},
"bounds": {
"neighbour": {
"attributes": ["peakEnd", "troughEnd", "slopeEnd"],
"between": ["start", "stop"],
"gap": 1,
"inner": { "cel": "visiblePhases(self) - 1" },
"absent": "spread",
"on": ["change", "handle"],
"pinned": "edited",
"doc": "Every visible phase, including those spread evenly around a dragged boundary, stays at least a month long; a boundary beyond the last visible phase is only kept inside the span."
}
}
},
"Trigger": {
"label": "Trigger",
"labelAttribute": "name",
"attributes": {
"name": { "type": "string", "required": true },
"date": { "type": "yearMonth", "required": true },
"row": { "type": "int", "default": 0 },
"tags": { "type": "string", "many": true },
"description": { "type": "text" },
"announcedAt": {
"type": "datetime",
"timezone": "floating",
"writtenPrecision": "preserve",
"newPrecision": "date",
"doc": "When the trigger was announced, as precisely as the source says: `2007-01-09` stays a date, `2007-01-09T09:41` keeps its minutes. A stand-in for timeline's `begin`."
},
"shippedAt": {
"type": "datetime",
"timezone": "floating",
"writtenPrecision": "preserve",
"newPrecision": "date",
"samePrecisionAs": "announcedAt",
"min": { "cel": "self.announcedAt" },
"outOfRange": { "drag": "clamp", "typed": "refuse", "message": "A trigger ships on or after the day it is announced." },
"doc": "Bounded by `announcedAt` and written in the same precision. A stand-in for timeline's `end`."
}
}
},
"Note": {
"label": "Note",
"labelAttribute": "text",
"doc": "Free text pinned to a date and a row. It has no tags and stays visible under every filter.",
"attributes": {
"text": { "type": "text", "default": "" },
"at": { "type": "yearMonth", "required": true },
"row": { "type": "int", "default": 0 }
}
}
},
"relations": {
"Influence": {
"label": "Influence",
"source": ["Trend", "Trigger"],
"target": "Trend",
"directed": true,
"allowSelfLoops": false,
"allowParallel": false,
"attributes": {
"fromPhase": { "type": "Phase", "doc": "Absent for an influence from a trigger." },
"fromEdge": { "type": "Edge" },
"fromAt": { "type": "number", "min": 0, "max": 1, "precision": 2 },
"toPhase": { "type": "Phase", "required": true },
"toEdge": { "type": "Edge", "required": true },
"toAt": { "type": "number", "required": true, "min": 0, "max": 1, "precision": 2 },
"description": { "type": "text" }
}
}
}
},
"coordinates": {
"axes": {
"time": {
"kind": "time",
"valueType": "yearMonth",
"label": "Time",
"origin": "1900-01",
"min": "-3200-01",
"scale": { "unit": { "attribute": "unit" }, "size": 4 },
"outOfRange": { "drag": "clamp", "typed": "refuse", "message": "Dates reach back to 3201 BCE (-3200-01) and no further." },
"ranges": [
{ "id": "history", "label": "History", "from": "-3200-01", "to": "1900-01", "style": { "fillOpacity": 0.08 }, "labelPosition": "start" },
{ "id": "modern", "label": "Modern", "from": "1900-01", "to": "2100-01", "separator": { "dash": [6, 6], "dashScale": "absolute" }, "labelPosition": "start" }
],
"endLabels": { "min": "Earlier", "max": "Later" },
"ruler": {
"visible": true,
"position": "bottom",
"attach": "view",
"levels": [
{ "unit": "month", "format": "MMM u", "minSpacingPx": 64 },
{ "unit": "quarter", "format": "MMM u", "minSpacingPx": 64 },
{ "unit": "year", "format": "u", "minSpacingPx": 64 },
{ "unit": "decade", "format": "u", "minSpacingPx": 64 },
{ "unit": "century", "format": "u", "minSpacingPx": 64 },
{ "unit": "millennium", "format": "u", "minSpacingPx": 64 },
{ "unit": "year", "step": 5000, "format": "u", "minSpacingPx": 64 }
],
"minUnit": { "attribute": "unit" }
},
"doc": "A month-index axis counted from 1900-01, four canvas units per step of the diagram's `unit`; `u` is the signed astronomical year. The ranges and end labels are a stand-in for wardley-map's named evolution stages and its Visible/Invisible end labels."
},
"rows": {
"kind": "linear",
"label": "Rows",
"unit": "row",
"scale": 56,
"origin": 0,
"min": 0,
"outOfRange": { "drag": "clamp", "typed": "refuse", "message": "Rows start at 0." }
},
"announced": {
"kind": "time",
"valueType": "datetime",
"timezone": "floating",
"scale": { "unit": "day", "size": 20 },
"ruler": {
"visible": true,
"position": "bottom",
"ticks": {
"minSpacingPx": 80,
"steps": [
{ "unit": "hour", "step": 1, "format": "HH:mm" },
{ "unit": "hour", "step": 6, "format": "HH:mm" },
{ "unit": "day", "step": 1, "format": "MMM d" },
{ "unit": "week", "step": 1, "format": "MMM d" },
{ "unit": "month", "step": 1, "format": "MMM yyyy" },
{ "unit": "quarter", "step": 1, "format": "MMM yyyy" },
{ "unit": "year", "step": 1, "format": "yyyy" }
]
},
"boundaryFormats": { "month": "MMM yyyy", "year": "yyyy" }
},
"doc": "The announcements viewpoint's date-time axis with one adaptive ruler row: a stand-in for timeline's ruler (a ruler has either `levels` or `ticks`)."
}
},
"systems": {
"hypeCycle": {
"kind": "cartesian",
"x": "time",
"y": "rows",
"orientation": "y-down",
"infinite": true,
"grid": { "visible": false },
"snapping": {
"x": { "calendar": { "unit": { "attribute": "unit" } }, "direction": "nearest", "ties": "away-from-zero" },
"y": { "grid": { "spacing": 1 }, "direction": "nearest", "ties": "away-from-zero" },
"reference": "bounds",
"byGesture": {
"drop": {
"x": { "calendar": { "unit": { "attribute": "unit" } }, "direction": "floor" },
"y": { "grid": { "spacing": 1 }, "direction": "floor" },
"reference": "center"
}
},
"doc": "A move or resize snaps to the nearest step start and row, halves away from zero on both sides of the origin; a toolbox drop lands in the step and row that contain the drop point (gartner-hype-cycle-graph.md, Snapping)."
}
},
"announcements": {
"kind": "cartesian",
"x": "announced",
"y": "rows",
"orientation": "y-down",
"snapping": {
"x": { "cel": "precisionOf(self, 'announcedAt') == 'date' ? value.startOf('day') : value" },
"doc": "A date-only value snaps to whole days; one written with a time keeps it."
}
}
},
"default": "hypeCycle"
},
"notation": {
"theme": {
"tokens": {
"ghg.peak": "#f7e7a1",
"ghg.trough": "#dfe3e8",
"ghg.slope": "#f6c99a",
"ghg.plateau": "#cde8a8",
"ghg.chevron": "#475569",
"ghg.trigger": "#7c3aed",
"ghg.note": "#fef9c3",
"ghg.noteText": "#0f172a",
"ghg.border": "#e2e8f0"
},
"modes": {
"dark": {
"ghg.peak": "#6b5a17",
"ghg.trough": "#475569",
"ghg.slope": "#7c4a1c",
"ghg.plateau": "#3f6212",
"ghg.chevron": "#e2e8f0",
"ghg.trigger": "#c4b5fd",
"ghg.note": "#423a10",
"ghg.noteText": "#f1f5f9",
"ghg.border": "#334155"
}
},
"followSystem": true
},
"styles": {
"segment": { "stroke": { "width": 0 } },
"dimmed": { "opacity": 0.25 }
},
"shapes": {
"phasedBanner": {
"label": "Phased banner",
"doc": "Simplified: up to four coloured segments side by side. The full definition cuts them with chevrons.",
"params": {
"count": { "type": "int", "default": 4, "min": 1, "max": 4, "unit": "count", "persist": "none" },
"b1": { "type": "number", "default": 0.25, "min": 0, "max": 1, "unit": "fraction", "persist": "none" },
"b2": { "type": "number", "default": 0.5, "min": 0, "max": 1, "unit": "fraction", "persist": "none" },
"b3": { "type": "number", "default": 0.75, "min": 0, "max": 1, "unit": "fraction", "persist": "none" }
},
"parts": [
{ "id": "peak", "shape": "rect", "style": { "extends": "segment", "fill": { "token": "ghg.peak" } }, "box": { "x": 0, "y": 0, "w": "p.count == 1 ? w : w * p.b1", "h": "h" }, "tooltip": "Peak of Inflated Expectations" },
{ "id": "trough", "when": "p.count >= 2", "shape": "rect", "style": { "extends": "segment", "fill": { "token": "ghg.trough" } }, "box": { "x": "w * p.b1", "y": 0, "w": "p.count == 2 ? w - w * p.b1 : w * (p.b2 - p.b1)", "h": "h" }, "tooltip": "Trough of Disillusionment" },
{ "id": "slope", "when": "p.count >= 3", "shape": "rect", "style": { "extends": "segment", "fill": { "token": "ghg.slope" } }, "box": { "x": "w * p.b2", "y": 0, "w": "p.count == 3 ? w - w * p.b2 : w * (p.b3 - p.b2)", "h": "h" }, "tooltip": "Slope of Enlightenment" },
{ "id": "plateau", "when": "p.count >= 4", "shape": "rect", "style": { "extends": "segment", "fill": { "token": "ghg.plateau" } }, "box": { "x": "w * p.b3", "y": 0, "w": "w - w * p.b3", "h": "h" }, "tooltip": "Plateau of Productivity" }
],
"handles": [
{
"param": "b1", "x": "w * p.b1", "y": "h / 2.0", "axis": "x",
"visible": "env.viewpoint == 'trueTime' && visiblePhases(self) >= 2",
"snap": { "cel": "snap(value, 1.0 / double(self.stop - self.start), 'away-from-zero')" },
"write": [ { "set": { "peakEnd": "fractionToMonth(self, value)" } } ],
"refusals": { "move": "Only a boundary between two visible phases can be moved." },
"label": { "cel": "'Peak ends ' + formatYearMonth(fractionToMonth(self, p.b1), 'MMM u')" }
},
{
"param": "b2", "x": "w * p.b2", "y": "h / 2.0", "axis": "x",
"visible": "env.viewpoint == 'trueTime' && visiblePhases(self) >= 3",
"snap": { "cel": "snap(value, 1.0 / double(self.stop - self.start), 'away-from-zero')" },
"write": [ { "set": { "troughEnd": "fractionToMonth(self, value)" } } ],
"refusals": { "move": "Only a boundary between two visible phases can be moved." },
"label": { "cel": "'Trough ends ' + formatYearMonth(fractionToMonth(self, p.b2), 'MMM u')" }
},
{
"param": "b3", "x": "w * p.b3", "y": "h / 2.0", "axis": "x",
"visible": "env.viewpoint == 'trueTime' && visiblePhases(self) >= 4",
"snap": { "cel": "snap(value, 1.0 / double(self.stop - self.start), 'away-from-zero')" },
"write": [ { "set": { "slopeEnd": "fractionToMonth(self, value)" } } ],
"refusals": { "move": "Only a boundary between two visible phases can be moved." },
"label": { "cel": "'Slope ends ' + formatYearMonth(fractionToMonth(self, p.b3), 'MMM u')" }
}
]
}
},
"nodes": {
"Trend": {
"shape": {
"type": "phasedBanner",
"params": {
"count": { "cel": "visiblePhases(self)" },
"b1": { "cel": "boundaryFraction(self, 1)" },
"b2": { "cel": "boundaryFraction(self, 2)" },
"b3": { "cel": "boundaryFraction(self, 3)" }
}
},
"size": { "default": [48, 32], "min": [null, 32], "max": [null, 32], "resizable": "horizontal" },
"placement": {
"system": "hypeCycle",
"x": { "attribute": "start" },
"x2": { "attribute": "stop" },
"y": { "attribute": "row" },
"anchor": "top-left",
"movable": { "x": true, "y": true },
"resizable": { "x": true, "y": false }
},
"labels": [
{ "id": "name", "text": { "attribute": "name" }, "position": "outside-left", "editable": "inline", "wrap": "none" }
],
"tooltip": { "cel": "self.name + ': ' + formatYearMonth(self.start, 'MMM u') + ' to ' + formatYearMonth(self.stop, 'MMM u') + ' (' + axisRange('time', self.start) + ')'" },
"states": { "filteredOut": "dimmed" }
},
"Trigger": {
"shape": "ellipse",
"style": { "fill": { "token": "ghg.trigger" }, "stroke": { "width": 0 } },
"size": { "fixed": [16, 16] },
"placement": { "system": "hypeCycle", "x": { "attribute": "date" }, "y": { "attribute": "row" }, "anchor": "center" },
"labels": [
{ "id": "name", "text": { "cel": "self.name + ' · ' + formatWhen(self.date, diagram.unit, false)" }, "editText": { "attribute": "name" }, "position": "outside-left", "editable": "inline", "wrap": "none" }
],
"anchors": { "mode": "fixed", "drawnFrom": "outline", "points": [ { "id": "n", "x": 0.5, "y": 0 }, { "id": "e", "x": 1, "y": 0.5 }, { "id": "s", "x": 0.5, "y": 1 } ] },
"states": { "filteredOut": "dimmed" }
},
"Note": {
"shape": "rect",
"style": {
"fill": { "token": "ghg.note" },
"stroke": { "color": { "token": "ghg.border" }, "width": 1 },
"font": { "color": { "token": "ghg.noteText" }, "size": 12 },
"padding": [4, 6]
},
"size": { "default": [160, 64], "resizable": true },
"placement": { "system": "hypeCycle", "x": { "attribute": "at" }, "y": { "attribute": "row" }, "anchor": "top-left" },
"labels": [
{ "id": "text", "text": { "attribute": "text" }, "position": "top-left", "editable": "multiline", "wrap": "word", "maxWidth": "parent", "overflow": "ellipsis" }
],
"connectable": false
}
},
"edges": {
"Influence": {
"line": {
"stroke": { "color": { "token": "ghg.chevron" }, "width": 1.5 },
"routing": "bezier",
"startDirection": "normal",
"endDirection": "normal"
},
"sourceMarker": "none",
"targetMarker": "arrow",
"anchoring": {
"source": {
"mode": "part",
"part": { "attribute": "fromPhase" },
"side": { "attribute": "fromEdge" },
"at": { "attribute": "fromAt" },
"movable": true,
"default": { "part": { "cel": "phaseName(visiblePhases(self.source) - 1)" }, "side": "bottom", "at": 0.5 }
},
"target": {
"mode": "part",
"part": { "attribute": "toPhase" },
"side": { "attribute": "toEdge" },
"at": { "attribute": "toAt" },
"movable": true,
"default": { "part": "peak", "side": "top", "at": 0.5 }
}
},
"variants": [
{
"when": "self.source.isA('Trigger')",
"anchoring": { "source": { "mode": "fixed", "points": [ { "id": "n", "x": 0.5, "y": 0 }, { "id": "e", "x": 1, "y": 0.5 }, { "id": "s", "x": 0.5, "y": 1 } ] } }
}
],
"doc": "An end is a phase, an edge and a fraction along that phase's stretch of the edge, bound to the relation's attributes: a resize or a boundary drag moves it proportionally, dragging it writes the attributes, and an end on a phase the trend does not draw hides the influence without removing it."
}
},
"canvas": {
"legend": { "visible": true, "position": "top-left", "entries": ["Phase"] },
"zoom": { "default": 1, "initial": "fit", "fitPadding": 0.1, "fitWhen": "firstContent" },
"filters": {
"tags": {
"label": "Filter by tags",
"control": "chips",
"appliesTo": ["Trend", "Trigger"],
"options": { "cel": "diagram.nodes.filter(n, n.isA('Trend') || n.isA('Trigger')).map(n, n.tags).flatten().map(t, lower(t)).distinct()" },
"default": [],
"match": { "default": "any", "userToggle": true },
"keep": "value.size() == 0 || (match == 'any' ? value.exists(t, self.tags.exists(s, lower(s) == lower(t))) : value.all(t, self.tags.exists(s, lower(s) == lower(t))))",
"effect": "hide",
"position": "outside-top-left",
"doc": "Hides trends and triggers without the chosen tags, and every influence to or from them. Notes always stay. View state only, never saved."
},
"find": {
"label": "Find",
"control": "search",
"appliesTo": ["Trend", "Trigger"],
"default": "",
"keep": "value == '' || lower(self.name).contains(lower(value))",
"effect": "dim",
"position": "outside-top-right",
"doc": "Dims what does not match (the `filteredOut` state) rather than hiding it."
}
},
"notices": [
{
"id": "filtered",
"position": "bottom-left",
"visible": { "cel": "filterValue('tags').size() > 0" },
"text": { "cel": "'Showing trends and triggers tagged ' + filterValue('tags').join(' or ') + '; notes always stay.'" },
"actions": [ { "label": "Clear the filter", "operation": "clearTagFilter" } ]
}
],
"doc": "The zoom is fitted once to the content, with 10 % to spare on each side, then left alone so an edit never rescales the drawing under the user (a stand-in for timeline)."
}
},
"toolbox": {
"groups": [
{
"id": "hypeCycle",
"label": "Hype cycle",
"tools": [
{
"id": "trend",
"creates": "Trend",
"label": "Trend",
"mode": "click",
"initial": { "name": "'New trend'", "phases": 4, "start": { "cel": "position != null ? position.x : yearMonth(2000, 1)" }, "stop": { "cel": "(position != null ? position.x : yearMonth(2000, 1)) + 12" } },
"after": "select",
"doc": "Dropped where it starts; it is a year long with all four phases."
},
{ "id": "trigger", "creates": "Trigger", "label": "Trigger", "mode": "click", "initial": { "name": "'Trigger'" }, "after": "select" },
{ "id": "note", "creates": "Note", "label": "Note", "mode": "click", "after": "editLabel" }
]
}
],
"contextTools": [
{
"for": ["Trend", "Trigger"],
"placement": "around",
"tools": [ { "kind": "connect", "via": "Influence", "label": "Influence" } ]
}
],
"contextMenus": [
{
"for": ["Trend"],
"placement": "menu",
"standardEntries": false,
"tools": [
{ "kind": "editLabel", "group": "edit", "label": "Rename…", "shortcut": "F2" },
{ "kind": "operation", "operation": "evenPhases", "group": "edit", "label": "Even phases" },
{ "kind": "delete", "group": "edit", "label": "Remove", "shortcut": "Delete" }
]
}
]
},
"forms": {
"trend": {
"for": "Trend",
"usage": ["inspector"],
"items": [
{ "attribute": "name", "required": true },
{
"attribute": "start",
"validate": [
{
"rule": "parseYearMonth(string(value)).hasValue()",
"message": { "cel": "'\\'' + string(value) + '\\' is not a date; write it as YYYY-MM, such as 2007-06.'" },
"timing": "commit"
}
]
},
{ "attribute": "stop" },
{
"attribute": "phases",
"label": "Phases",
"display": { "cel": "['Peak', 'Peak and Trough', 'Peak, Trough and Slope', 'All four'][visiblePhases(self) - 1]" },
"parse": {
"accepts": "value.trim() in ['Peak', 'Peak and Trough', 'Peak, Trough and Slope', 'All four']",
"write": [ { "set": { "phases": "value.trim() == 'Peak' ? 1 : (value.trim() == 'Peak and Trough' ? 2 : (value.trim() == 'Peak, Trough and Slope' ? 3 : 4))" } } ],
"refusal": { "cel": "'\\'' + value + '\\' is not one of Peak, Peak and Trough, Peak, Trough and Slope, All four.'" }
}
},
{ "attribute": "tags", "widget": "tags" }
]
}
},
"constraints": {
"order": [
"ghg.duplicate-influence", "ghg.self-influence", "ghg.stop-before-start", "ghg.phase-count", "ghg.boundary-order", "ghg.bad-attachment",
"ghg.duplicate-id", "ghg.unreadable-entry", "ghg.influence-into-trigger", "ghg.mixed-precision"
],
"builtIn": {
"std.facets": { "severity": "warning" },
"std.endpoints": {
"severity": "warning",
"code": { "cel": "cel.bind(v, detail.?violation.orValue(self.target.isA('Trigger') ? 'target' : (self.source == self.target ? 'selfLoop' : 'parallel')), v == 'target' ? 'ghg.influence-into-trigger' : (v == 'selfLoop' ? 'ghg.self-influence' : 'ghg.duplicate-influence'))" },
"oncePerGroup": "second",
"doc": "Covers ghg.duplicate-influence, ghg.self-influence and ghg.influence-into-trigger, which the standalone tool reports under their own codes; the code is computed per finding, by detail.violation. A repeat is reported once per pair, at the second influence."
},
"std.duplicateId": {
"code": "ghg.duplicate-id",
"message": { "cel": "'`' + detail.id + '` is declared ' + string(detail.?count.orValue(2)) + ' times; an id names one entry.'" },
"oncePerGroup": "last",
"doc": "Ids are unique across all four lists together. An id held several times is reported once, at its last holder."
},
"std.unreadableEntry": { "code": "ghg.unreadable-entry", "message": { "cel": "'This entry could not be read and is not drawn (' + (detail.entry.path != '' ? detail.entry.path : detail.entry.text) + '): ' + detail.reason" } },
"std.mixedPrecision": { "code": "ghg.mixed-precision", "message": "A trigger's announcement and shipping dates are both dates, or both carry a time." }
},
"rules": [
{
"id": "stopAfterStart",
"code": "ghg.stop-before-start",
"scope": "Trend",
"when": "has(self.start) && has(self.stop)",
"rule": "self.stop > self.start",
"attribute": "stop",
"message": { "cel": "'`' + self.id + '` stops at ' + formatYearMonth(self.stop, 'u-MM') + ', not after it starts at ' + formatYearMonth(self.start, 'u-MM') + '; a trend is at least a month long.'" }
},
{
"id": "phaseCount",
"code": "ghg.phase-count",
"scope": "Trend",
"rule": "self.phases >= 1 && self.phases <= 4",
"attribute": "phases",
"message": { "cel": "'`' + self.id + '` shows ' + string(self.phases) + ' phases; a trend shows 1 to 4.'" }
},
{
"id": "boundaryOrder",
"code": "ghg.boundary-order",
"scope": "Trend",
"when": "has(self.start) && has(self.stop) && self.stop > self.start",
"rule": "cel.bind(b, [self.start] + (has(self.peakEnd) ? [self.peakEnd] : []) + (has(self.troughEnd) ? [self.troughEnd] : []) + (has(self.slopeEnd) ? [self.slopeEnd] : []) + [self.stop], lists.range(b.size() - 1).all(i, b[i] < b[i + 1]))",
"message": { "cel": "'`' + self.id + '` has a phase boundary that is out of order or outside ' + formatYearMonth(self.start, 'u-MM') + ' to ' + formatYearMonth(self.stop, 'u-MM') + '.'" }
},
{
"id": "sourceAttachment",
"code": "ghg.bad-attachment",
"scope": "Influence",
"when": "self.source.isA('Trend')",
"rule": "has(self.fromPhase) && has(self.fromEdge) && has(self.fromAt)",
"message": { "cel": "'`' + self.id + '`\\'s from end does not name a phase, a top or bottom edge, and an `at` from 0 to 1.'" }
}
]
},
"behavior": {
"operations": {
"evenPhases": {
"label": "Even phases",
"for": "Trend",
"unavailable": [
{ "when": "!has(self.peakEnd) && !has(self.troughEnd) && !has(self.slopeEnd)", "message": "This trend's phases are already even." }
],
"actions": [ { "unset": ["peakEnd", "troughEnd", "slopeEnd"] } ]
},
"arrange": {
"label": "Arrange diagram",
"for": "diagram",
"actions": [
{
"layout": {
"algorithm": "arrangeRows",
"refusals": { "nothingDrawn": "There is nothing to arrange until this graph has a trend.", "unchanged": "This graph is already arranged." }
}
}
],
"doc": "Puts every trend, trigger and note on the row that leaves the graph least cluttered, as one undoable step; only rows change, never a date."
},
"clearTagFilter": {
"label": "Clear the filter",
"for": "diagram",
"actions": [ { "view": { "filters": { "tags": "[]" } } } ]
}
},
"deletion": {
"Trend": {
"relations": "delete",
"confirm": {
"count": "self.incoming.size() + self.outgoing.size()",
"threshold": 1,
"message": { "cel": "'Removing this trend also removes the ' + (count == 1 ? 'influence' : string(count) + ' influences') + ' to or from it.'" },
"confirmLabel": "Remove"
}
}
}
},
"layout": {
"algorithms": {
"compactRows": {
"algorithm": "rowPacked",
"scope": "all",
"rowPacked": { "keepRows": true, "order": "start", "gap": 4, "followConnections": "Influence", "targetAfter": "sourceMiddle" },
"doc": "The compact viewpoint's layout: every element keeps its row and is packed left in start order, 4 canvas units apart, an influence's target after the middle of its source."
},
"arrangeRows": {
"algorithm": "rows",
"rows": {
"writes": ["row"],
"extent": {
"Trend": { "from": "x - 8.0 - textWidth(self.name, 12.0)", "to": "x2" },
"Trigger": { "from": "x - 8.0 - textWidth(self.name + ' · ' + formatWhen(self.date, diagram.unit, false), 12.0)", "to": "x2" },
"Note": { "from": "x", "to": "x + width", "rows": "int(math.ceil(height / 56.0))" }
},
"clearance": 16,
"affinity": "Influence",
"ties": "lower",
"swapPasses": 64
},
"doc": "Arrange diagram: rows only, because across is the data."
}
},
"trigger": "manual",
"respect": "all"
},
"viewpoints": {
"trueTime": {
"label": "True time",
"default": true,
"coordinateSystem": "hypeCycle",
"doc": "Every element at its dates on the time axis."
},
"compact": {
"label": "Compact",
"variantOf": "trueTime",
"toggle": { "label": "Compact", "icon": "mdi-arrow-collapse-horizontal", "position": "outside-top-left" },
"coordinateSystem": "hypeCycle",
"layout": "compactRows",
"notation": {
"nodes": {
"Trend": {
"shape": {
"type": "phasedBanner",
"params": {
"count": { "cel": "visiblePhases(self)" },
"b1": { "cel": "1.0 / double(visiblePhases(self))" },
"b2": { "cel": "2.0 / double(visiblePhases(self))" },
"b3": { "cel": "3.0 / double(visiblePhases(self))" }
}
},
"size": { "width": { "cel": "24.0 * double(visiblePhases(self))" }, "height": 32, "resizable": false },
"placement": { "system": "hypeCycle", "x": { "layout": true }, "y": { "attribute": "row" }, "anchor": "top-left", "movable": false, "resizable": false }
}
}
},
"doc": "Trends shortened to 24 canvas units per visible phase, packed left in their rows. A toggle on true-time views, not a view kind: it is viewer state, never saved, and every diagram opens in true time."
},
"announcements": {
"label": "Announcements",
"coordinateSystem": "announcements",
"include": ["Trigger"],
"notation": {
"nodes": {
"Trigger": {
"placement": { "system": "announcements", "x": { "attribute": "announcedAt" }, "y": { "attribute": "row" }, "anchor": "center" }
}
}
},
"canvas": { "bounds": { "x": 0, "y": 0, "w": 1000, "h": 600 }, "fit": "stretch", "margin": [48, 24, 48, 24] },
"doc": "Triggers by their announcement date on an adaptive ruler, the canvas stretched to fill the pane. A stand-in for timeline (the ruler) and wardley-map (the stretched canvas)."
}
},
"persistence": {
"ids": {
"strategy": "uuid-v4",
"encoding": "base64url",
"stable": true,
"doc": "New entries get a ShortGuid: a version 4 GUID written as 22 characters of URL-safe base64. Hand-written ids such as `steam-engine` are kept as written."
},
"view": { "store": [], "viewer": ["viewpoint", "filters", "viewport"] }
}
}In an IDE
No IDE host runs this example yet.