gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
472 lines (454 loc) • 23.5 kB
HTML
<html>
<head>
<meta charset="UTF-8">
<title>Control Instruments: Gauges and Meters</title>
<meta name="description" content="Gauges and Meters that allow the user to control the values that those instruments show" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
<script src="../release/go.js"></script>
<script src="../assets/js/goSamples.js"></script> <!-- this is only for the GoJS Samples framework -->
<script id="code">
function init() {
if (window.goSamples) goSamples(); // init for these samples -- you don"t need to call this
var $ = go.GraphObject.make;
myDiagram = $(go.Diagram, "myDiagramDiv",
{ "undoManager.isEnabled": true });
// These properties are what change an object from being a value indicator,
// such as a needle or a bar or a thumb of a slider, to being a controller
// that the user can drag to change the value of the instrument.
// This assumes that the scale (a "Graduated" Panel) is named "SCALE".
// The alwaysVisible parameter determines whether the object's visibility
// is controlled by the "SCALE"'s Panel.isEnabled property.
function sliderActions(alwaysVisible) {
return [
{
isActionable: true,
actionDown: function(e, obj) {
obj._dragging = true;
obj._original = obj.part.data.value;
},
actionMove: function(e, obj) {
if (!obj._dragging) return;
var scale = obj.part.findObject("SCALE");
var pt = e.diagram.lastInput.documentPoint;
var loc = scale.getLocalPoint(pt);
var val = Math.round(scale.graduatedValueForPoint(loc));
// just set the data.value temporarily, not recorded in UndoManager
e.diagram.model.commit(function(m) {
m.set(obj.part.data, "value", val);
}, null); // null means skipsUndoManager
},
actionUp: function(e, obj) {
if (!obj._dragging) return;
obj._dragging = false;
var scale = obj.part.findObject("SCALE");
var pt = e.diagram.lastInput.documentPoint;
var loc = scale.getLocalPoint(pt);
var val = Math.round(scale.graduatedValueForPoint(loc));
e.diagram.model.commit(function(m) {
m.set(obj.part.data, "value", obj._original);
}, null); // null means skipsUndoManager
// now set the data.value for real
e.diagram.model.commit(function(m) {
m.set(obj.part.data, "value", val);
}, "dragged slider");
},
actionCancel: function(e, obj) {
obj._dragging = false;
e.diagram.model.commit(function(m) {
m.set(obj.part.data, "value", obj._original);
}, null); // null means skipsUndoManager
}
},
(alwaysVisible ? {} : new go.Binding("visible", "isEnabled").ofObject("SCALE")),
new go.Binding("cursor", "isEnabled", function(e) { return e ? "pointer" : ""; }).ofObject("SCALE")
];
}
// These helper functions simplify the node templates
function commonScaleBindings() {
return [
new go.Binding("graduatedMin", "min"),
new go.Binding("graduatedMax", "max"),
new go.Binding("graduatedTickUnit", "unit"),
new go.Binding("isEnabled", "editable")
];
}
function commonSlider(vert) {
return $(go.Shape, "RoundedRectangle",
{
name: "SLIDER",
fill: "white",
desiredSize: (vert ? new go.Size(20, 6) : new go.Size(6, 20)),
alignment: (vert ? go.Spot.Top : go.Spot.Right)
},
sliderActions(false)
);
}
function commonNodeStyle() {
return [
{ locationSpot: go.Spot.Center },
{ fromSpot: go.Spot.BottomRightSides, toSpot: go.Spot.TopLeftSides },
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
];
}
myDiagram.nodeTemplateMap.add("Horizontal",
$(go.Node, "Auto", commonNodeStyle(),
// {
// resizable: true,
// resizeObjectName: "PATH",
// resizeAdornmentTemplate:
// $(go.Adornment, "Spot",
// $(go.Placeholder),
// $(go.Shape, { fill: "dodgerblue", width: 8, height: 8, alignment: go.Spot.Right, cursor: "e-resize" }))
// },
$(go.Shape,
{ fill: "lightgray", stroke: "gray" }),
$(go.Panel, "Table",
{ margin: 1, stretch: go.GraphObject.Fill },
// header information
$(go.TextBlock,
{ row: 0, font: "bold 10pt sans-serif" },
new go.Binding("text")),
$(go.Panel, "Spot",
{ row: 1 },
$(go.Panel, "Graduated",
{ name: "SCALE", margin: new go.Margin(0, 6), graduatedTickUnit: 10, isEnabled: false },
commonScaleBindings(),
$(go.Shape, { geometryString: "M0 0 H200", height: 0, name: "PATH" }),
$(go.Shape, { geometryString: "M0 0 V16", alignmentFocus: go.Spot.Center, stroke: "gray" }),
$(go.Shape, { geometryString: "M0 0 V20", alignmentFocus: go.Spot.Center, interval: 5, strokeWidth: 1.5 })
),
$(go.Panel, "Spot",
{ alignment: go.Spot.Left, alignmentFocus: go.Spot.Left, alignmentFocusName: "BAR" },
// the indicator (a bar)
$(go.Shape,
{ name: "BAR", fill: "red", strokeWidth: 0, height: 8 },
new go.Binding("fill", "color"),
new go.Binding("desiredSize", "value", function(v, shp) {
var scale = shp.part.findObject("SCALE");
var path = scale.findMainElement();
var len = (v-scale.graduatedMin) / (scale.graduatedMax-scale.graduatedMin) * path.geometry.bounds.width;
return new go.Size(len, 10);
})),
commonSlider(false)
)
),
// state information
$(go.TextBlock, "0",
{ row: 2, alignment: go.Spot.Left },
new go.Binding("text", "min")),
$(go.TextBlock, "100",
{ row: 2, alignment: go.Spot.Right },
new go.Binding("text", "max")),
$(go.TextBlock,
{ row: 2, background: "white", font: "bold 10pt sans-serif", isMultiline: false, editable: true },
new go.Binding("text", "value", function(v) { return v.toString(); }).makeTwoWay(function(s) { return parseFloat(s); }))
)
));
myDiagram.nodeTemplateMap.add("Vertical",
$(go.Node, "Auto", commonNodeStyle(),
// {
// resizable: true,
// resizeObjectName: "PATH",
// resizeAdornmentTemplate:
// $(go.Adornment, "Spot",
// $(go.Placeholder),
// $(go.Shape, { fill: "dodgerblue", width: 8, height: 8, alignment: go.Spot.Top, cursor: "n-resize" }))
// },
$(go.Shape,
{ fill: "lightgray", stroke: "gray" }),
$(go.Panel, "Table",
{ margin: 1, stretch: go.GraphObject.Fill },
// header information
$(go.TextBlock,
{ row: 0, font: "bold 10pt sans-serif" },
new go.Binding("text")),
$(go.Panel, "Spot",
{ row: 1 },
$(go.Panel, "Graduated",
{ name: "SCALE", margin: new go.Margin(6, 0), graduatedTickUnit: 10, isEnabled: false },
commonScaleBindings(),
// NOTE: path goes upward!
$(go.Shape, { geometryString: "M0 0 V-200", width: 0, name: "PATH" }),
$(go.Shape, { geometryString: "M0 0 V16", alignmentFocus: go.Spot.Center, stroke: "gray" }),
$(go.Shape, { geometryString: "M0 0 V20", alignmentFocus: go.Spot.Center, interval: 5, strokeWidth: 1.5 })
),
$(go.Panel, "Spot",
{ alignment: go.Spot.Bottom, alignmentFocus: go.Spot.Bottom, alignmentFocusName: "BAR" },
// the indicator (a bar)
$(go.Shape,
{ name: "BAR", fill: "red", strokeWidth: 0, height: 8 },
new go.Binding("fill", "color"),
new go.Binding("desiredSize", "value", function(v, shp) {
var scale = shp.part.findObject("SCALE");
var path = scale.findMainElement();
var len = (v-scale.graduatedMin) / (scale.graduatedMax-scale.graduatedMin) * path.geometry.bounds.height;
return new go.Size(10, len);
})),
commonSlider(true)
)
),
// state information
$(go.TextBlock, "0",
{ row: 2, alignment: go.Spot.Left },
new go.Binding("text", "min")),
$(go.TextBlock, "100",
{ row: 2, alignment: go.Spot.Right },
new go.Binding("text", "max")),
$(go.TextBlock,
{ row: 2, background: "white", font: "bold 10pt sans-serif", isMultiline: false, editable: true },
new go.Binding("text", "value", function(v) { return v.toString(); }).makeTwoWay(function(s) { return parseFloat(s); }))
)
));
myDiagram.nodeTemplateMap.add("NeedleMeter",
$(go.Node, "Auto", commonNodeStyle(),
$(go.Shape, { fill: "darkslategray" }),
$(go.Panel, "Spot",
$(go.Panel, "Position",
$(go.Panel, "Graduated",
{ name: "SCALE", margin: 10 },
commonScaleBindings(),
$(go.Shape, { name: "PATH", geometryString: "M0 0 A120 120 0 0 1 200 0", stroke: "white" }),
$(go.Shape, { geometryString: "M0 0 V10", stroke: "white" }),
$(go.TextBlock,
{ segmentOffset: new go.Point(0, 12), segmentOrientation: go.Link.OrientAlong, stroke: "white" })
),
$(go.Shape,
{ stroke: "red", strokeWidth: 4, isGeometryPositioned: true },
new go.Binding("geometry", "value", function(v, shp) {
var scale = shp.part.findObject("SCALE");
var pt = scale.graduatedPointForValue(v);
var geo = new go.Geometry(go.Geometry.Line);
geo.startX = 100 + scale.margin.left;
geo.startY = 90 + scale.margin.top;
geo.endX = pt.x + scale.margin.left;
geo.endY = pt.y + scale.margin.top;
return geo;
}),
sliderActions(true))
),
$(go.TextBlock,
{ alignment: new go.Spot(0.5, 0.5, 0, 20), stroke: "white", font: "bold 10pt sans-serif" },
new go.Binding("text"),
new go.Binding("stroke", "color")),
$(go.TextBlock,
{ alignment: go.Spot.Top, margin: new go.Margin(4, 0, 0, 0) },
{ stroke: "white", font: "bold italic 13pt sans-serif", isMultiline: false, editable: true },
new go.Binding("text", "value", function(v) { return v.toString(); }).makeTwoWay(function(s) { return parseFloat(s); }),
new go.Binding("stroke", "color"))
)
));
myDiagram.nodeTemplateMap.add("CircularMeter",
$(go.Node, "Table", commonNodeStyle(),
$(go.Panel, "Auto",
{ row: 0 },
$(go.Shape, "Circle",
{ stroke: "orange", strokeWidth: 5, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight },
new go.Binding("stroke", "color")),
$(go.Panel, "Spot",
$(go.Panel, "Graduated",
{
name: "SCALE", margin: 14,
graduatedTickUnit: 2.5, // tick marks at each multiple of 2.5
stretch: go.GraphObject.None // needed to avoid unnecessary re-measuring!!!
},
commonScaleBindings(),
// the main path of the graduated panel, an arc starting at 135 degrees and sweeping for 270 degrees
$(go.Shape, { name: "PATH", geometryString: "M-70.7 70.7 B135 270 0 0 100 100 M0 100", stroke: "white", strokeWidth: 4 }),
// three differently sized tick marks
$(go.Shape, { geometryString: "M0 0 V10", stroke: "white", strokeWidth: 1 }),
$(go.Shape, { geometryString: "M0 0 V12", stroke: "white", strokeWidth: 2, interval: 2 }),
$(go.Shape, { geometryString: "M0 0 V15", stroke: "white", strokeWidth: 3, interval: 4 }),
$(go.TextBlock,
{ // each tick label
interval: 4,
alignmentFocus: go.Spot.Center,
font: "bold italic 14pt sans-serif", stroke: "white",
segmentOffset: new go.Point(0, 30)
})
),
$(go.TextBlock,
{ alignment: new go.Spot(0.5, 0.9), stroke: "white", font: "bold italic 14pt sans-serif", editable: true },
new go.Binding("text", "value", function(v) { return v.toString(); }).makeTwoWay(function(s) { return parseFloat(s); }),
new go.Binding("stroke", "color")),
$(go.Shape, { fill: "red", strokeWidth: 0, geometryString: "F1 M-6 0 L0 -6 100 0 0 6z x M-100 0" },
new go.Binding("angle", "value", function(v, shp) {
// this determines the angle of the needle, based on the data.value argument
var scale = shp.part.findObject("SCALE");
var p = scale.graduatedPointForValue(v);
var path = shp.part.findObject("PATH");
var c = path.actualBounds.center;
return c.directionPoint(p);
}),
sliderActions(true)),
$(go.Shape, "Circle", { width: 2, height: 2, fill: "#444" })
)
),
$(go.TextBlock,
{ row: 1, font: "bold 11pt sans-serif" },
new go.Binding("text"))
));
myDiagram.nodeTemplateMap.add("BarMeter",
$(go.Node, "Table", commonNodeStyle(),
{ scale: 0.8 },
$(go.Panel, "Auto",
{ row: 0 },
$(go.Shape, "Circle",
{ stroke: "orange", strokeWidth: 5, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight },
new go.Binding("stroke", "color")),
$(go.Panel, "Spot",
$(go.Panel, "Graduated",
{
name: "SCALE", margin: 14,
graduatedTickUnit: 2.5, // tick marks at each multiple of 2.5
stretch: go.GraphObject.None // needed to avoid unnecessary re-measuring!!!
},
commonScaleBindings(),
// the main path of the graduated panel, an arc starting at 135 degrees and sweeping for 270 degrees
$(go.Shape, { name: "PATH", geometryString: "M-70.7 70.7 B135 270 0 0 100 100 M0 100", stroke: "white", strokeWidth: 4 }),
// three differently sized tick marks
$(go.Shape, { geometryString: "M0 0 V10", stroke: "white", strokeWidth: 1 }),
$(go.Shape, { geometryString: "M0 0 V12", stroke: "white", strokeWidth: 2, interval: 2 }),
$(go.Shape, { geometryString: "M0 0 V15", stroke: "white", strokeWidth: 3, interval: 4 }),
$(go.TextBlock,
{ // each tick label
interval: 4,
alignmentFocus: go.Spot.Center,
font: "bold italic 14pt sans-serif", stroke: "white",
segmentOffset: new go.Point(0, 30)
})
),
$(go.TextBlock,
{ alignment: go.Spot.Center, stroke: "white", font: "bold italic 14pt sans-serif", editable: true },
new go.Binding("text", "value", function(v) { return v.toString(); }).makeTwoWay(function(s) { return parseFloat(s); }),
new go.Binding("stroke", "color")),
$(go.Shape, { fill: "red", strokeWidth: 0 },
new go.Binding("geometry", "value", function(v, shp) {
var scale = shp.part.findObject("SCALE");
var p0 = scale.graduatedPointForValue(scale.graduatedMin);
var pv = scale.graduatedPointForValue(v);
var path = shp.part.findObject("PATH");
var radius = path.actualBounds.width/2;
var c = path.actualBounds.center;
var a0 = c.directionPoint(p0);
var av = c.directionPoint(pv);
var sweep = av-a0;
if (sweep < 0) sweep += 360;
var layerThickness = 8;
return new go.Geometry()
.add(new go.PathFigure(-radius, -radius)) // always make sure the Geometry includes the top left corner
.add(new go.PathFigure(radius, radius)) // and the bottom right corner of the whole circular area
.add(new go.PathFigure(p0.x-radius, p0.y-radius)
.add(new go.PathSegment(go.PathSegment.Arc, a0, sweep, 0, 0, radius, radius))
.add(new go.PathSegment(go.PathSegment.Line, pv.x-radius, pv.y-radius))
.add(new go.PathSegment(go.PathSegment.Arc, av, -sweep, 0, 0, radius-layerThickness, radius-layerThickness).close()));
}),
sliderActions(true)),
$(go.Shape, "Circle", { width: 2, height: 2, fill: "#444" })
)
),
$(go.TextBlock,
{ row: 1, font: "bold 11pt sans-serif" },
new go.Binding("text"))
));
myDiagram.linkTemplate =
$(go.Link,
{ routing: go.Link.AvoidsNodes, corner: 12 },
$(go.Shape, { isPanelMain: true, stroke: "gray", strokeWidth: 9 }),
$(go.Shape, { isPanelMain: true, stroke: "lightgray", strokeWidth: 5 }),
$(go.Shape, { isPanelMain: true, stroke: "whitesmoke" })
)
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, value: 87, text: "Vertical", category: "Vertical", loc: "30 0", editable: true, color: "yellow" },
{ key: 2, value: 23, text: "Circular Meter", category: "CircularMeter", loc: "250 -120", editable: true, color: "skyblue" },
{ key: 3, value: 56, text: "Needle Meter", category: "NeedleMeter", loc: "250 110", editable: true, color: "lightsalmon" },
{ key: 4, value: 16, max: 120, text: "Horizontal", category: "Horizontal", loc: "550 0", editable: true, color: "green" },
{ key: 5, value: 23, max: 200, unit: 5, text: "Bar Meter", category: "BarMeter", loc: "550 200", editable: true, color: "orange" }
],
[
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 2, to: 4 },
{ from: 3, to: 4 },
{ from: 4, to: 5 }
]);
loop(); // start a simple simulation
}
function loop() {
setTimeout(function() {
myDiagram.commit(function() {
myDiagram.links.each(function(l) {
if (Math.random() < 0.2) return;
var prev = l.fromNode.data.value;
var now = l.toNode.data.value;
if (prev > (l.fromNode.data.min || 0) && now < (l.toNode.data.max || 100)) {
myDiagram.model.set(l.fromNode.data, "value", prev-1);
myDiagram.model.set(l.toNode.data, "value", now+1);
}
});
})
loop();
}, 500);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:800px; height:600px"></div>
<p>
Instruments are Panels that include:
</p>
<ul>
<li>a scale which is a "Graduated" Panel showing a possible range of values</li>
<li>one or more indicators that show the instrument's value</li>
</ul>
<p>
Optionally there are other TextBlocks or Shapes that show additional information.
Indicators can be Shapes or TextBlocks or more complicated Panels.
For more about scales, please read <a href="../intro/graduatedPanels.html">Graduated Panels</a>.
For simplicity, all of these instruments only show one value.
But you could define instruments that show multiple values on the same scale,
or that have multiple scales.
</p>
<p>
When an instrument is also a control, the user can modify the instrument's value.
When the instrument is editable, there may be a handle that the user can drag.
This might be the same as the indicator or might be a different object.
</p>
<p>
This sample defines five different types of instruments.
<ul>
<li><b>Horizontal</b>, a horizontal scale with a bar indicator and a slider handle</li>
<li><b>Vertical</b>, a vertical scale with a bar indicator and a slider handle</li>
<li><b>NeedleMeter</b>, a curved scale with a straight needle indicator</li>
<li><b>CircularMeter</b>, a circular scale with a polygonal needle indicator</li>
<li><b>BarMeter</b>, a circular scale with an annular bar indicator</li>
</ul>
<p>
The value to be shown by the instrument is assumed to be the <code>data.value</code> property.
The value is shown both textually in a TextBlock and graphically using an indicator on the scale.
If the value of <code>data.editable</code> is true,
</p>
<ul>
<li>
the user can drag something to change the instrument's value --
the value is limited by the <a>Panel.graduatedMin</a> and <a>Panel.graduatedMax</a> values
</li>
<li>the user can in-place edit the TextBlock showing the value (if the node is selected, hit the F2 key)</li>
</ul>
<p>
Of course you can change the details of anything you want to use.
You might want to add more TextBlocks to show more information.
A few properties already have data Bindings, such as:
</p>
<ul>
<li><a>TextBlock.text</a> from <code>data.text</code>, for the name of the instrument</li>
<li><a>Panel.graduatedMin</a> from <code>data.min</code>, to control the range of the scale</li>
<li><a>Panel.graduatedMax</a> from <code>data.max</code>, to control the range of the scale</li>
<li>(various) from <code>data.color</code>, to control some colors used by the instrument</li>
</ul>
</div>
</body>
</html>