flocc 0.5.21 → 0.5.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/flocc.js CHANGED
@@ -823,7 +823,37 @@
823
823
  Operators["agent"] = "agent";
824
824
  Operators["environment"] = "environment";
825
825
  Operators["vector"] = "vector";
826
+ Operators["log"] = "log";
826
827
  })(Operators || (Operators = {}));
828
+ var operatorInfo = {
829
+ add: { min: 2, max: 2 },
830
+ subtract: { min: 2, max: 2 },
831
+ multiply: { min: 2, max: 2 },
832
+ divide: { min: 2, max: 2 },
833
+ mod: { min: 2, max: 2 },
834
+ power: { min: 2, max: 2 },
835
+ get: { min: 1, max: 1 },
836
+ set: { min: 2, max: 2 },
837
+ enqueue: { min: 2, max: 2 },
838
+ local: { min: 1, max: 2 },
839
+ if: { min: 2, max: 3 },
840
+ and: { min: 2, max: 2 },
841
+ or: { min: 2, max: 2 },
842
+ gt: { min: 2, max: 2 },
843
+ gte: { min: 2, max: 2 },
844
+ lt: { min: 2, max: 2 },
845
+ lte: { min: 2, max: 2 },
846
+ eq: { min: 2, max: 2 },
847
+ map: { min: 2, max: 2 },
848
+ filter: { min: 2, max: 2 },
849
+ key: { min: 2, max: 2 },
850
+ method: { min: 2, max: Infinity },
851
+ agent: { min: 0, max: 0 },
852
+ environment: { min: 0, max: 0 },
853
+ vector: { min: 1, max: Infinity },
854
+ log: { min: 1, max: Infinity }
855
+ };
856
+ var ARITHMETIC_OPS = new Set(["add", "subtract", "multiply", "divide", "mod", "power"]);
827
857
  var add = function (a, b) { return a + b; };
828
858
  var subtract = function (a, b) { return a - b; };
829
859
  var multiply = function (a, b) { return a * b; };
@@ -847,6 +877,127 @@
847
877
  return null;
848
878
  return obj[name].apply(obj, args);
849
879
  };
880
+ /**
881
+ * Format a single value for step display. If isFirst is true and the value is
882
+ * a known operator string, render it bare (unquoted).
883
+ */
884
+ function formatStepValue(val, isFirst) {
885
+ if (val === null || val === undefined)
886
+ return String(val);
887
+ if (typeof val === "number" || typeof val === "boolean")
888
+ return String(val);
889
+ if (typeof val === "string") {
890
+ if (isFirst && val in Operators)
891
+ return val;
892
+ return JSON.stringify(val);
893
+ }
894
+ if (val instanceof Array) {
895
+ if (val.length === 0)
896
+ return "[]";
897
+ var parts = val.map(function (s, i) { return formatStepValue(s, i === 0); });
898
+ return "(" + parts.join(" ") + ")";
899
+ }
900
+ if (typeof val === "object") {
901
+ try {
902
+ return JSON.stringify(val);
903
+ }
904
+ catch (_a) {
905
+ return String(val);
906
+ }
907
+ }
908
+ return String(val);
909
+ }
910
+ /**
911
+ * Format a step into a readable string representation for logging.
912
+ */
913
+ function formatStep(step) {
914
+ return formatStepValue(step, false);
915
+ }
916
+ /**
917
+ * Format a value as an S-expression atom (no recursion into arrays).
918
+ */
919
+ function formatAtom(val) {
920
+ if (val === null || val === undefined)
921
+ return "null";
922
+ if (typeof val === "number" || typeof val === "boolean")
923
+ return String(val);
924
+ if (typeof val === "string")
925
+ return JSON.stringify(val);
926
+ if (typeof val === "object" && !Array.isArray(val)) {
927
+ try {
928
+ return JSON.stringify(val);
929
+ }
930
+ catch (_a) {
931
+ return "[object]";
932
+ }
933
+ }
934
+ return String(val);
935
+ }
936
+ /**
937
+ * Format a value for S-expression display. The first element (operator) of
938
+ * a step is rendered as a bare identifier; other string values are quoted.
939
+ */
940
+ function formatSexpValue(val, isOperator) {
941
+ if (isOperator && typeof val === "string")
942
+ return val;
943
+ return formatAtom(val);
944
+ }
945
+ /**
946
+ * Pretty-print a single step as an S-expression with width-based wrapping.
947
+ */
948
+ function prettyFormatStep(step, indentUnit, maxLineWidth, currentIndent) {
949
+ if (!Array.isArray(step))
950
+ return formatAtom(step);
951
+ if (step.length === 0)
952
+ return "()";
953
+ // Try compact first
954
+ var compact = "(" + step.map(function (s, i) {
955
+ if (Array.isArray(s))
956
+ return prettyFormatStep(s, indentUnit, Infinity, "");
957
+ return formatSexpValue(s, i === 0);
958
+ }).join(" ") + ")";
959
+ if (currentIndent.length + compact.length <= maxLineWidth)
960
+ return compact;
961
+ // Wrap: operator on first line, each arg indented
962
+ var op = Array.isArray(step[0]) ? prettyFormatStep(step[0], indentUnit, maxLineWidth, currentIndent) : formatSexpValue(step[0], true);
963
+ var args = step.slice(1);
964
+ if (args.length === 0)
965
+ return "(" + op + ")";
966
+ var childIndent = currentIndent + indentUnit;
967
+ var formattedArgs = args.map(function (a) { return prettyFormatStep(a, indentUnit, maxLineWidth, childIndent); });
968
+ return "(" + op + "\n" + formattedArgs.map(function (a) { return childIndent + a; }).join("\n") + ")";
969
+ }
970
+ function validOperatorNames() {
971
+ return Object.keys(operatorInfo).join(", ");
972
+ }
973
+ function checkArity(op, argCount, strict) {
974
+ if (strict === void 0) { strict = false; }
975
+ var info = operatorInfo[op];
976
+ if (!info)
977
+ return null;
978
+ if (info.min === info.max) {
979
+ if (argCount < info.min) {
980
+ return "Rule: \"" + op + "\" expects " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
981
+ }
982
+ if (strict && argCount > info.max) {
983
+ return "Rule: \"" + op + "\" expects " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
984
+ }
985
+ }
986
+ else if (info.max === Infinity) {
987
+ if (argCount < info.min) {
988
+ return "Rule: \"" + op + "\" expects at least " + info.min + " argument" + (info.min !== 1 ? "s" : "") + ", got " + argCount;
989
+ }
990
+ }
991
+ else {
992
+ if (argCount < info.min) {
993
+ return "Rule: \"" + op + "\" expects " + info.min + "-" + info.max + " arguments, got " + argCount;
994
+ }
995
+ if (strict && argCount > info.max) {
996
+ return "Rule: \"" + op + "\" expects " + info.min + "-" + info.max + " arguments, got " + argCount;
997
+ }
998
+ }
999
+ return null;
1000
+ }
850
1001
  /**
851
1002
  * The `Rule` class is an experimental interface for adding behavior to {@linkcode Agent}s. A `Rule` object may be used in place of a `tick` function to be added as `Agent` behavior using `agent.set('tick', tickRule)`. As a trivial example, consider the following `Rule`, which increments the `Agent`'s `x` value with every time step:
852
1003
  *
@@ -901,6 +1052,7 @@
901
1052
  * |`"agent"`|`0`|No arguments; returns the `Agent`|
902
1053
  * |`"environment"`|`0`|No arguments, returns the `Environment`|
903
1054
  * |`"vector"`|`any`|Creates an n-dimensional {@linkcode Vector} from the supplied arguments|
1055
+ * |`"log"`|`any`|Logs expression(s) and returns the last evaluated value|
904
1056
  */
905
1057
  function Rule(environment, steps) {
906
1058
  var _this = this;
@@ -909,10 +1061,12 @@
909
1061
  /** @hidden */
910
1062
  this.locals = {};
911
1063
  /**
912
- * interpret single array step
913
- * @since 0.3.0
914
- * @hidden
1064
+ * When true, evaluation traces are logged and captured.
915
1065
  */
1066
+ this.trace = false;
1067
+ this.traceLog = [];
1068
+ /** @hidden */
1069
+ this._traceDepth = 0;
916
1070
  this.evaluate = function (agent, step) {
917
1071
  var first = step && step.length > 0 ? step[0] : null;
918
1072
  if (first === undefined || first === null)
@@ -923,30 +1077,79 @@
923
1077
  return _this.evaluate(agent, step.slice(1));
924
1078
  return innerStep;
925
1079
  }
926
- if (first === "log") {
927
- console.log("logging", step.slice(1), _this.evaluate(agent, step.slice(1)));
928
- return null;
1080
+ // Trace: check if this is a known operator call to trace
1081
+ var shouldTrace = _this.trace && typeof first === "string" && first in Operators;
1082
+ if (shouldTrace)
1083
+ _this._traceDepth++;
1084
+ var result = _this._evaluateOp(agent, step, first);
1085
+ if (shouldTrace) {
1086
+ _this._traceLogEntry(step, result);
1087
+ _this._traceDepth--;
1088
+ }
1089
+ return result;
1090
+ };
1091
+ /** @hidden */
1092
+ this._evaluateOp = function (agent, step, first) {
1093
+ if (first === Operators.log) {
1094
+ var args = step.slice(1);
1095
+ if (args.length === 0) {
1096
+ console.warn("Rule: \"log\" expects at least 1 argument, got 0");
1097
+ return null;
1098
+ }
1099
+ var lastValue = null;
1100
+ for (var i = 0; i < args.length; i++) {
1101
+ var expr = args[i];
1102
+ var evaluated = _this.evaluate(agent, [expr]);
1103
+ console.log("Rule log: " + formatStep(expr) + " \u2192 " + formatStep(evaluated));
1104
+ lastValue = evaluated;
1105
+ }
1106
+ return lastValue;
929
1107
  }
930
1108
  if (!(first in Operators) && step.length > 1) {
1109
+ if (typeof first === "string") {
1110
+ console.warn("Rule: unknown operator \"" + first + "\". Valid operators: " + validOperatorNames());
1111
+ }
931
1112
  return step;
932
1113
  }
1114
+ var argCount = step.length - 1;
1115
+ // Arity check at runtime for known operators
1116
+ if (typeof first === "string" && first in Operators) {
1117
+ var arityMsg = checkArity(first, argCount);
1118
+ if (arityMsg) {
1119
+ console.warn(arityMsg);
1120
+ }
1121
+ }
933
1122
  var a = step.length > 1 ? [step[1]] : undefined;
934
1123
  var b = step.length > 2 ? [step[2]] : undefined;
935
1124
  var c = step.length > 3 ? [step[3]] : undefined;
936
- if (first === Operators.add)
937
- return add(_this.evaluate(agent, a), _this.evaluate(agent, b));
938
- if (first === Operators.subtract)
939
- return subtract(_this.evaluate(agent, a), _this.evaluate(agent, b));
940
- if (first === Operators.multiply)
941
- return multiply(_this.evaluate(agent, a), _this.evaluate(agent, b));
942
- if (first === Operators.divide)
943
- return divide(_this.evaluate(agent, a), _this.evaluate(agent, b));
944
- if (first === Operators.mod)
945
- return mod(_this.evaluate(agent, a), _this.evaluate(agent, b));
946
- if (first === Operators.power)
947
- return power(_this.evaluate(agent, a), _this.evaluate(agent, b));
948
- if (first === Operators.get)
949
- return get(agent, _this.evaluate(agent, a));
1125
+ // Arithmetic operators with type checking
1126
+ if (ARITHMETIC_OPS.has(first)) {
1127
+ var va = _this.evaluate(agent, a);
1128
+ var vb = _this.evaluate(agent, b);
1129
+ if (typeof va !== "number" || typeof vb !== "number") {
1130
+ console.warn("Rule: \"" + first + "\" expects numeric arguments, got " + typeof va + " and " + typeof vb);
1131
+ }
1132
+ if (first === Operators.add)
1133
+ return add(va, vb);
1134
+ if (first === Operators.subtract)
1135
+ return subtract(va, vb);
1136
+ if (first === Operators.multiply)
1137
+ return multiply(va, vb);
1138
+ if (first === Operators.divide)
1139
+ return divide(va, vb);
1140
+ if (first === Operators.mod)
1141
+ return mod(va, vb);
1142
+ if (first === Operators.power)
1143
+ return power(va, vb);
1144
+ }
1145
+ if (first === Operators.get) {
1146
+ var keyName = _this.evaluate(agent, a);
1147
+ var result = get(agent, keyName);
1148
+ if (result === null || result === undefined) {
1149
+ console.warn("Rule: \"get\" returned null for key \"" + keyName + "\" \u2014 key may not exist on agent");
1150
+ }
1151
+ return result;
1152
+ }
950
1153
  if (first === Operators.set)
951
1154
  return set(agent, _this.evaluate(agent, a), _this.evaluate(agent, b));
952
1155
  if (first === Operators.enqueue) {
@@ -1034,13 +1237,151 @@
1034
1237
  this.environment = environment;
1035
1238
  this.steps = steps;
1036
1239
  }
1240
+ /**
1241
+ * Format arbitrary steps as pretty-printed S-expressions.
1242
+ */
1243
+ Rule.formatSteps = function (steps, options) {
1244
+ var indent = (options && options.indent) || " ";
1245
+ var maxLineWidth = (options && options.maxLineWidth) || 60;
1246
+ // Check if it's multi-step (array of arrays)
1247
+ var isMultiStep = steps.length > 0 && Array.isArray(steps[0]);
1248
+ if (isMultiStep) {
1249
+ return steps.map(function (s) { return prettyFormatStep(s, indent, maxLineWidth, ""); }).join("\n\n");
1250
+ }
1251
+ return prettyFormatStep(steps, indent, maxLineWidth, "");
1252
+ };
1253
+ /**
1254
+ * Pretty-print the rule's step tree as S-expressions.
1255
+ */
1256
+ Rule.prototype.format = function (options) {
1257
+ return Rule.formatSteps(this.steps, options);
1258
+ };
1259
+ /**
1260
+ * Returns a formatted S-expression representation of the rule.
1261
+ */
1262
+ Rule.prototype.toString = function () {
1263
+ return this.format();
1264
+ };
1265
+ /**
1266
+ * Validate the rule's step tree and return an array of diagnostics.
1267
+ * Does not throw — returns diagnostics for inspection.
1268
+ * @since 0.6.0
1269
+ */
1270
+ Rule.prototype.validate = function () {
1271
+ var diagnostics = [];
1272
+ if (!this.steps || (Array.isArray(this.steps) && this.steps.length === 0)) {
1273
+ diagnostics.push({
1274
+ path: "root",
1275
+ level: "warning",
1276
+ message: "Empty steps array"
1277
+ });
1278
+ return diagnostics;
1279
+ }
1280
+ // Check if steps is a single step (first element is a string operator)
1281
+ // or an array of steps (first element is an array)
1282
+ var isMultiStep = Array.isArray(this.steps[0]);
1283
+ if (isMultiStep) {
1284
+ // Multiple top-level steps
1285
+ for (var i = 0; i < this.steps.length; i++) {
1286
+ var step = this.steps[i];
1287
+ if (!Array.isArray(step)) {
1288
+ diagnostics.push({
1289
+ path: "[" + i + "]",
1290
+ level: "warning",
1291
+ message: "Step is a bare value (" + typeof step + ": " + JSON.stringify(step) + ") instead of an array"
1292
+ });
1293
+ }
1294
+ else {
1295
+ this._validateStep(step, "[" + i + "]", diagnostics);
1296
+ }
1297
+ }
1298
+ }
1299
+ else {
1300
+ // Single step (the steps array IS the step)
1301
+ this._validateStep(this.steps, "root", diagnostics);
1302
+ }
1303
+ return diagnostics;
1304
+ };
1305
+ /** @hidden */
1306
+ Rule.prototype._validateStep = function (step, path, diagnostics) {
1307
+ if (step.length === 0) {
1308
+ diagnostics.push({
1309
+ path: path,
1310
+ level: "warning",
1311
+ message: "Empty step array"
1312
+ });
1313
+ return;
1314
+ }
1315
+ var first = step[0];
1316
+ // If first element is an array, it's a nested step sequence
1317
+ if (Array.isArray(first)) {
1318
+ this._validateStep(first, path + "[0]", diagnostics);
1319
+ for (var i = 1; i < step.length; i++) {
1320
+ if (Array.isArray(step[i])) {
1321
+ this._validateStep(step[i], path + ("[" + i + "]"), diagnostics);
1322
+ }
1323
+ }
1324
+ return;
1325
+ }
1326
+ if (typeof first === "string") {
1327
+ var argCount = step.length - 1;
1328
+ // Check if operator is known
1329
+ if (!(first in Operators)) {
1330
+ if (step.length > 1) {
1331
+ diagnostics.push({
1332
+ path: path,
1333
+ level: "error",
1334
+ message: "Unknown operator \"" + first + "\". Valid operators: " + validOperatorNames()
1335
+ });
1336
+ }
1337
+ return;
1338
+ }
1339
+ // Check arity (strict: also warn on too many args)
1340
+ var arityMsg = checkArity(first, argCount, true);
1341
+ if (arityMsg) {
1342
+ diagnostics.push({
1343
+ path: path,
1344
+ level: "error",
1345
+ message: arityMsg
1346
+ });
1347
+ }
1348
+ // Recurse into sub-steps
1349
+ for (var i = 1; i < step.length; i++) {
1350
+ if (Array.isArray(step[i])) {
1351
+ this._validateStep(step[i], path + ("[" + i + "]"), diagnostics);
1352
+ }
1353
+ }
1354
+ }
1355
+ };
1356
+ /**
1357
+ * interpret single array step
1358
+ * @since 0.3.0
1359
+ * @hidden
1360
+ */
1361
+ /** @hidden */
1362
+ Rule.prototype._traceLogEntry = function (step, result) {
1363
+ var indent = " ".repeat(this._traceDepth - 1);
1364
+ var formatted = formatStep(step);
1365
+ var resultFormatted = formatStep(result);
1366
+ var line = "Rule trace: " + indent + formatted + " \u2192 " + resultFormatted;
1367
+ console.log(line);
1368
+ this.traceLog.push(line);
1369
+ };
1037
1370
  /**
1038
1371
  * @since 0.3.0
1039
1372
  * @hidden
1040
1373
  */
1041
1374
  Rule.prototype.call = function (agent) {
1375
+ if (this.trace) {
1376
+ this.traceLog = [];
1377
+ this._traceDepth = 0;
1378
+ }
1042
1379
  return this.evaluate(agent, this.steps);
1043
1380
  };
1381
+ /**
1382
+ * Static operator info mapping operator names to their expected argument counts.
1383
+ */
1384
+ Rule.operatorInfo = operatorInfo;
1044
1385
  return Rule;
1045
1386
  }());
1046
1387
 
@@ -3218,6 +3559,15 @@
3218
3559
  * such as a {@linkcode LineChartRenderer}, {@linkcode Histogram}, etc.
3219
3560
  */
3220
3561
  _this.renderers = [];
3562
+ /**
3563
+ * Whether the `Environment` tick cycle is currently playing.
3564
+ * Use {@linkcode pause}, {@linkcode resume}, or {@linkcode toggle}
3565
+ * to control playback.
3566
+ * @since 0.5.22
3567
+ */
3568
+ _this.playing = true;
3569
+ /** @hidden */
3570
+ _this._tickIntervalId = null;
3221
3571
  /**
3222
3572
  * This property will always equal the number of tick cycles that
3223
3573
  * have passed since the `Environment` was created. If you call
@@ -3387,6 +3737,9 @@
3387
3737
  * @since 0.0.5
3388
3738
  */
3389
3739
  Environment.prototype.tick = function (opts) {
3740
+ // If paused, skip the tick cycle (use `step()` to advance manually)
3741
+ if (!this.playing)
3742
+ return;
3390
3743
  var _a = this._getTickOptions(opts), activation = _a.activation, activationCount = _a.activationCount, count = _a.count, randomizeOrder = _a.randomizeOrder;
3391
3744
  // for uniform activation, every agent is always activated
3392
3745
  if (activation === "uniform") {
@@ -3454,6 +3807,47 @@
3454
3807
  }
3455
3808
  this.renderers.forEach(function (r) { return r.render(); });
3456
3809
  };
3810
+ /**
3811
+ * Pause the tick cycle. While paused, calling {@linkcode tick} will
3812
+ * be a no-op unless you use {@linkcode step} to advance manually.
3813
+ * @since 0.5.22
3814
+ */
3815
+ Environment.prototype.pause = function () {
3816
+ this.playing = false;
3817
+ };
3818
+ /**
3819
+ * Resume the tick cycle after it has been paused.
3820
+ * @since 0.5.22
3821
+ */
3822
+ Environment.prototype.resume = function () {
3823
+ this.playing = true;
3824
+ };
3825
+ /**
3826
+ * Toggle the tick cycle between playing and paused.
3827
+ * @since 0.5.22
3828
+ */
3829
+ Environment.prototype.toggle = function () {
3830
+ this.playing = !this.playing;
3831
+ };
3832
+ /**
3833
+ * Advance the `Environment` by exactly one tick, regardless of whether
3834
+ * it is paused. This is useful for stepping through the simulation
3835
+ * frame-by-frame while paused.
3836
+ *
3837
+ * ```js
3838
+ * environment.pause();
3839
+ * environment.step(); // advances one tick
3840
+ * ```
3841
+ *
3842
+ * @since 0.5.22
3843
+ */
3844
+ Environment.prototype.step = function (opts) {
3845
+ // Temporarily mark as playing so tick executes, then restore
3846
+ var wasPlaying = this.playing;
3847
+ this.playing = true;
3848
+ this.tick(opts);
3849
+ this.playing = wasPlaying;
3850
+ };
3457
3851
  /**
3458
3852
  * Use a helper with this environment. A helper can be one of:
3459
3853
  * - {@linkcode KDTree}
@@ -3990,7 +4384,10 @@
3990
4384
  width: 500,
3991
4385
  height: 500,
3992
4386
  scale: 1,
3993
- trace: false
4387
+ trace: false,
4388
+ interactive: false,
4389
+ zoomMin: 0.1,
4390
+ zoomMax: 10
3994
4391
  };
3995
4392
  /**
3996
4393
  * A `CanvasRenderer` renders an {@linkcode Environment} spatially in two dimensions.
@@ -4010,6 +4407,12 @@
4010
4407
  * - `"triangle"` &mdash; Draws a triangle centered at the `Agent`'s `"x"` / `"y"` values.
4011
4408
  * - Also uses the `"size"` value.
4012
4409
  *
4410
+ * When `interactive` is set to `true` in the options, the renderer supports:
4411
+ * - **Click/hover detection** &mdash; Use {@linkcode on} to listen for `"click"`, `"hover"`, and `"unhover"` events on agents.
4412
+ * - **Agent selection** &mdash; Clicking an agent selects it (highlighted with a stroke). Access selected agents via {@linkcode selected}.
4413
+ * - **Pan** &mdash; Click and drag on empty space to pan.
4414
+ * - **Zoom** &mdash; Scroll to zoom in/out (bounded by `zoomMin` / `zoomMax`).
4415
+ *
4013
4416
  * @since 0.0.11
4014
4417
  */
4015
4418
  var CanvasRenderer = /** @class */ (function (_super) {
@@ -4025,15 +4428,33 @@
4025
4428
  * - `connectionOpacity` (*number* = `1`) &mdash; For `Environment`s using a `Network`, the opacity of lines
4026
4429
  * - `connectionWidth` (*number* = `1`) &mdash; For `Environment`s using a `Network`, the width of lines
4027
4430
  * - `height` (*number* = `500`) &mdash; The height, in pixels, of the canvas on which to render
4431
+ * - `interactive` (*boolean* = `false`) &mdash; Enables interactive features (click/hover detection, selection, pan, zoom)
4432
+ * - `onSelect` (*function*) &mdash; Optional callback when an agent is selected or deselected
4028
4433
  * - `origin` (*{ x: number; y: number }* = `{ x: 0, y: 0 }`) &mdash; The coordinate of the upper-left point of the space to be rendered
4029
4434
  * - `scale` (*number* = `1`) &mdash; The scale at which to render (the larger the scale, the smaller the size of the space that is actually rendered)
4030
4435
  * - `trace` (*boolean* = `false`) &mdash; If `true`, the renderer will not clear old drawings, causing the `Agent`s to appear to *trace* their paths across space
4031
4436
  * - `width` (*number* = `500`) &mdash; The width, in pixels, of the canvas on which to render
4437
+ * - `zoomMin` (*number* = `0.1`) &mdash; Minimum scale when zooming
4438
+ * - `zoomMax` (*number* = `10`) &mdash; Maximum scale when zooming
4032
4439
  */
4033
4440
  function CanvasRenderer(environment, opts) {
4034
4441
  var _this = _super.call(this) || this;
4035
4442
  /** @hidden */
4036
4443
  _this.terrainBuffer = document.createElement("canvas");
4444
+ /** The currently selected agents (only used when `interactive` is `true`). */
4445
+ _this.selected = [];
4446
+ /** @hidden */
4447
+ _this._listeners = new Map();
4448
+ /** @hidden */
4449
+ _this._hoveredAgent = null;
4450
+ /** @hidden */
4451
+ _this._isPanning = false;
4452
+ /** @hidden */
4453
+ _this._panStart = null;
4454
+ /** @hidden */
4455
+ _this._panOriginStart = null;
4456
+ /** @hidden */
4457
+ _this._boundHandlers = {};
4037
4458
  _this.environment = environment;
4038
4459
  environment.renderers.push(_this);
4039
4460
  _this.opts = Object.assign({}, defaultOptions);
@@ -4050,9 +4471,158 @@
4050
4471
  _this.terrainBuffer.width = width;
4051
4472
  _this.terrainBuffer.height = height;
4052
4473
  _this.context.fillStyle = opts.background;
4053
- _this.context.fillRect(0, 0, width, height);
4474
+ _this.context.fillRect(0, 0, _this.width, _this.height);
4475
+ if (_this.opts.interactive) {
4476
+ _this._setupInteractiveListeners();
4477
+ }
4054
4478
  return _this;
4055
4479
  }
4480
+ /**
4481
+ * Register a callback for an interactive event.
4482
+ * Supported event names: `"click"`, `"hover"`, `"unhover"`.
4483
+ *
4484
+ * ```js
4485
+ * renderer.on("click", (agent, event) => {
4486
+ * console.log("Clicked agent:", agent.id);
4487
+ * });
4488
+ * ```
4489
+ *
4490
+ * @param eventName - The event to listen for.
4491
+ * @param callback - The callback, invoked with the `Agent` and the `MouseEvent`.
4492
+ */
4493
+ CanvasRenderer.prototype.on = function (eventName, callback) {
4494
+ if (!this._listeners.has(eventName)) {
4495
+ this._listeners.set(eventName, []);
4496
+ }
4497
+ this._listeners.get(eventName).push(callback);
4498
+ };
4499
+ /** @hidden */
4500
+ CanvasRenderer.prototype._emit = function (eventName, agent, event) {
4501
+ var callbacks = this._listeners.get(eventName);
4502
+ if (callbacks) {
4503
+ callbacks.forEach(function (cb) { return cb(agent, event); });
4504
+ }
4505
+ };
4506
+ /**
4507
+ * Given a mouse event, return the agent at that position (if any).
4508
+ * Hit-testing accounts for the agent's shape and size.
4509
+ * @hidden
4510
+ */
4511
+ CanvasRenderer.prototype._agentAtPoint = function (clientX, clientY) {
4512
+ var rect = this.canvas.getBoundingClientRect();
4513
+ var dpr = window.devicePixelRatio;
4514
+ var canvasX = (clientX - rect.left) * dpr;
4515
+ var canvasY = (clientY - rect.top) * dpr;
4516
+ var agents = this.environment.getAgents();
4517
+ // Iterate in reverse so topmost-drawn agent is found first
4518
+ for (var i = agents.length - 1; i >= 0; i--) {
4519
+ var agent = agents[i];
4520
+ var data = agent.getData();
4521
+ var ax = this.x(data.x);
4522
+ var ay = this.y(data.y);
4523
+ var shape = data.shape;
4524
+ var size = (data.size || 1) * dpr;
4525
+ if (shape === "rect") {
4526
+ var w = (data.width || 1) * dpr;
4527
+ var h = (data.height || 1) * dpr;
4528
+ var rx = ax - w / 2;
4529
+ var ry = ay - h / 2;
4530
+ if (canvasX >= rx && canvasX <= rx + w && canvasY >= ry && canvasY <= ry + h) {
4531
+ return agent;
4532
+ }
4533
+ }
4534
+ else if (shape === "triangle") {
4535
+ // Simple bounding-box hit test for triangles
4536
+ var halfSize = size / 2;
4537
+ if (canvasX >= ax - halfSize &&
4538
+ canvasX <= ax + halfSize &&
4539
+ canvasY >= ay - halfSize &&
4540
+ canvasY <= ay + halfSize) {
4541
+ return agent;
4542
+ }
4543
+ }
4544
+ else {
4545
+ // Default: circle (and arrow) — distance-based hit test
4546
+ var dx = canvasX - ax;
4547
+ var dy = canvasY - ay;
4548
+ var hitRadius = Math.max(size, 4 * dpr); // minimum hit area for tiny agents
4549
+ if (dx * dx + dy * dy <= hitRadius * hitRadius) {
4550
+ return agent;
4551
+ }
4552
+ }
4553
+ }
4554
+ return null;
4555
+ };
4556
+ /** @hidden */
4557
+ CanvasRenderer.prototype._setupInteractiveListeners = function () {
4558
+ var _this = this;
4559
+ var onMouseDown = function (e) {
4560
+ var agent = _this._agentAtPoint(e.clientX, e.clientY);
4561
+ if (agent) {
4562
+ // Agent click — select it
4563
+ _this.selected = [agent];
4564
+ if (_this.opts.onSelect)
4565
+ _this.opts.onSelect(agent);
4566
+ _this._emit("click", agent, e);
4567
+ _this.render();
4568
+ }
4569
+ else {
4570
+ // Empty space — deselect and start panning
4571
+ if (_this.selected.length > 0) {
4572
+ _this.selected = [];
4573
+ if (_this.opts.onSelect)
4574
+ _this.opts.onSelect(null);
4575
+ _this.render();
4576
+ }
4577
+ _this._isPanning = true;
4578
+ _this._panStart = { x: e.clientX, y: e.clientY };
4579
+ _this._panOriginStart = { x: _this.opts.origin.x, y: _this.opts.origin.y };
4580
+ }
4581
+ };
4582
+ var onMouseMove = function (e) {
4583
+ if (_this._isPanning && _this._panStart && _this._panOriginStart) {
4584
+ var dpr = window.devicePixelRatio;
4585
+ var dx = e.clientX - _this._panStart.x;
4586
+ var dy = e.clientY - _this._panStart.y;
4587
+ _this.opts.origin = {
4588
+ x: _this._panOriginStart.x - dx / (_this.opts.scale * dpr),
4589
+ y: _this._panOriginStart.y - dy / (_this.opts.scale * dpr)
4590
+ };
4591
+ _this.render();
4592
+ return;
4593
+ }
4594
+ // Hover detection
4595
+ var agent = _this._agentAtPoint(e.clientX, e.clientY);
4596
+ if (agent !== _this._hoveredAgent) {
4597
+ if (_this._hoveredAgent) {
4598
+ _this._emit("unhover", _this._hoveredAgent, e);
4599
+ }
4600
+ if (agent) {
4601
+ _this._emit("hover", agent, e);
4602
+ }
4603
+ _this._hoveredAgent = agent;
4604
+ }
4605
+ };
4606
+ var onMouseUp = function (e) {
4607
+ _this._isPanning = false;
4608
+ _this._panStart = null;
4609
+ _this._panOriginStart = null;
4610
+ };
4611
+ var onWheel = function (e) {
4612
+ e.preventDefault();
4613
+ var _a = _this.opts, zoomMin = _a.zoomMin, zoomMax = _a.zoomMax;
4614
+ var delta = e.deltaY > 0 ? 0.9 : 1.1;
4615
+ var newScale = _this.opts.scale * delta;
4616
+ newScale = Math.max(zoomMin, Math.min(zoomMax, newScale));
4617
+ _this.opts.scale = newScale;
4618
+ _this.render();
4619
+ };
4620
+ this._boundHandlers = { mousedown: onMouseDown, mousemove: onMouseMove, mouseup: onMouseUp, wheel: onWheel };
4621
+ this.canvas.addEventListener("mousedown", onMouseDown);
4622
+ this.canvas.addEventListener("mousemove", onMouseMove);
4623
+ this.canvas.addEventListener("mouseup", onMouseUp);
4624
+ this.canvas.addEventListener("wheel", onWheel, { passive: false });
4625
+ };
4056
4626
  /** @hidden */
4057
4627
  CanvasRenderer.prototype.x = function (v) {
4058
4628
  var _a = this.opts, origin = _a.origin, scale = _a.scale;
@@ -4089,21 +4659,21 @@
4089
4659
  };
4090
4660
  /** @hidden */
4091
4661
  CanvasRenderer.prototype.drawPathWrap = function (points) {
4092
- var _this = this;
4093
4662
  var _a = this, width = _a.width, height = _a.height;
4094
4663
  var right = false;
4095
4664
  var left = false;
4096
4665
  var lower = false;
4097
4666
  var upper = false;
4667
+ // points are already in DPR-scaled pixel space, so compare directly
4098
4668
  points.forEach(function (_a) {
4099
4669
  var px = _a[0], py = _a[1];
4100
- if (_this.x(px) >= width)
4670
+ if (px >= width)
4101
4671
  right = true;
4102
- if (_this.x(px) < 0)
4672
+ if (px < 0)
4103
4673
  left = true;
4104
- if (_this.y(py) >= height)
4674
+ if (py >= height)
4105
4675
  lower = true;
4106
- if (_this.y(py) < 0)
4676
+ if (py < 0)
4107
4677
  upper = true;
4108
4678
  });
4109
4679
  if (right)
@@ -4132,24 +4702,26 @@
4132
4702
  /** @hidden */
4133
4703
  CanvasRenderer.prototype.drawCircleWrap = function (x, y, size) {
4134
4704
  var _a = this, width = _a.width, height = _a.height;
4705
+ var worldWidth = this.opts.width;
4706
+ var worldHeight = this.opts.height;
4135
4707
  if (this.x(x + size) >= width) {
4136
- this.drawCircle(x - width, y, size);
4708
+ this.drawCircle(x - worldWidth, y, size);
4137
4709
  if (this.y(y + size) >= height)
4138
- this.drawCircle(x - width, y - height, size);
4710
+ this.drawCircle(x - worldWidth, y - worldHeight, size);
4139
4711
  if (this.y(y - size) < 0)
4140
- this.drawCircle(x - width, y + height, size);
4712
+ this.drawCircle(x - worldWidth, y + worldHeight, size);
4141
4713
  }
4142
4714
  if (this.x(x - size) < 0) {
4143
- this.drawCircle(x + width, y, size);
4715
+ this.drawCircle(x + worldWidth, y, size);
4144
4716
  if (this.y(y + size) >= height)
4145
- this.drawCircle(x + width, y - height, size);
4717
+ this.drawCircle(x + worldWidth, y - worldHeight, size);
4146
4718
  if (this.y(y - size) < 0)
4147
- this.drawCircle(x + width, y + height, size);
4719
+ this.drawCircle(x + worldWidth, y + worldHeight, size);
4148
4720
  }
4149
4721
  if (this.y(y + size) > height)
4150
- this.drawCircle(x, y - height, size);
4722
+ this.drawCircle(x, y - worldHeight, size);
4151
4723
  if (this.y(y - size) < 0)
4152
- this.drawCircle(x, y + height, size);
4724
+ this.drawCircle(x, y + worldHeight, size);
4153
4725
  };
4154
4726
  /**
4155
4727
  * Draw a rectangle centered at (x, y). Automatically calculates the offset
@@ -4163,25 +4735,55 @@
4163
4735
  };
4164
4736
  /** @hidden */
4165
4737
  CanvasRenderer.prototype.drawRectWrap = function (x, y, w, h) {
4166
- var _a = this.opts, width = _a.width, height = _a.height;
4738
+ var _a = this, width = _a.width, height = _a.height;
4739
+ var worldWidth = this.opts.width;
4740
+ var worldHeight = this.opts.height;
4167
4741
  if (this.x(x + w / 2) >= width) {
4168
- this.drawRect(x - width, y, w, h);
4742
+ this.drawRect(x - worldWidth, y, w, h);
4169
4743
  if (this.y(y + h / 2) >= height)
4170
- this.drawRect(x - width, y - height, w, h);
4171
- if (this.y(y - height / 2) < 0)
4172
- this.drawRect(x - width, y + height, w, h);
4744
+ this.drawRect(x - worldWidth, y - worldHeight, w, h);
4745
+ if (this.y(y - h / 2) < 0)
4746
+ this.drawRect(x - worldWidth, y + worldHeight, w, h);
4173
4747
  }
4174
4748
  if (this.x(x - w / 2) < 0) {
4175
- this.drawRect(x + width, y, w, h);
4749
+ this.drawRect(x + worldWidth, y, w, h);
4176
4750
  if (this.y(y + h / 2) >= height)
4177
- this.drawRect(x + width, y - height, w, h);
4178
- if (this.y(y - height / 2) < 0)
4179
- this.drawRect(x + width, y + height, w, h);
4751
+ this.drawRect(x + worldWidth, y - worldHeight, w, h);
4752
+ if (this.y(y - h / 2) < 0)
4753
+ this.drawRect(x + worldWidth, y + worldHeight, w, h);
4180
4754
  }
4181
4755
  if (this.y(y + h / 2) > height)
4182
- this.drawRect(x, y - height, w, h);
4183
- if (this.y(y - height / 2) < 0)
4184
- this.drawRect(x, y + height, w, h);
4756
+ this.drawRect(x, y - worldHeight, w, h);
4757
+ if (this.y(y - h / 2) < 0)
4758
+ this.drawRect(x, y + worldHeight, w, h);
4759
+ };
4760
+ /**
4761
+ * Draw a selection highlight around the given agent.
4762
+ * @hidden
4763
+ */
4764
+ CanvasRenderer.prototype._drawSelectionHighlight = function (agent) {
4765
+ var bufferContext = this.buffer.getContext("2d");
4766
+ var dpr = window.devicePixelRatio;
4767
+ var data = agent.getData();
4768
+ var ax = this.x(data.x);
4769
+ var ay = this.y(data.y);
4770
+ var shape = data.shape;
4771
+ var size = (data.size || 1) * dpr;
4772
+ bufferContext.save();
4773
+ bufferContext.strokeStyle = "#0af";
4774
+ bufferContext.lineWidth = 2 * dpr;
4775
+ if (shape === "rect") {
4776
+ var w = (data.width || 1) * dpr;
4777
+ var h = (data.height || 1) * dpr;
4778
+ bufferContext.strokeRect(ax - w / 2 - 2 * dpr, ay - h / 2 - 2 * dpr, w + 4 * dpr, h + 4 * dpr);
4779
+ }
4780
+ else {
4781
+ bufferContext.beginPath();
4782
+ var highlightRadius = Math.max(size, 4 * dpr) + 3 * dpr;
4783
+ bufferContext.arc(ax, ay, highlightRadius, 0, 2 * Math.PI);
4784
+ bufferContext.stroke();
4785
+ }
4786
+ bufferContext.restore();
4185
4787
  };
4186
4788
  CanvasRenderer.prototype.render = function () {
4187
4789
  var _this = this;
@@ -4194,22 +4796,24 @@
4194
4796
  // if "trace" is truthy, don't clear the canvas with every frame
4195
4797
  // to trace the paths of agents
4196
4798
  if (!trace) {
4197
- context.clearRect(0, 0, width * dpr, height * dpr);
4799
+ context.clearRect(0, 0, width, height);
4198
4800
  context.fillStyle = opts.background;
4199
- context.fillRect(0, 0, width * dpr, height * dpr);
4801
+ context.fillRect(0, 0, width, height);
4200
4802
  }
4201
4803
  // automatically position agents in an environment that uses a network helper
4202
4804
  if (opts.autoPosition && environment.helpers.network) {
4203
4805
  environment.getAgents().forEach(function (agent) {
4204
4806
  var network = _this.environment.helpers.network;
4205
- var _a = _this, width = _a.width, height = _a.height;
4807
+ // Use CSS pixel dimensions (opts), not the DPI-scaled canvas dimensions,
4808
+ // since x() and y() already apply the devicePixelRatio transform.
4809
+ var _a = _this.opts, w = _a.width, h = _a.height;
4206
4810
  // only set once
4207
4811
  if ((agent.get("x") === null || agent.get("y") === null) &&
4208
4812
  network.isInNetwork(agent)) {
4209
4813
  var idx = network.indexOf(agent);
4210
4814
  var angle = idx / network.agents.length;
4211
- var x = width / 2 + 0.4 * width * Math.cos(2 * Math.PI * angle);
4212
- var y = height / 2 + 0.4 * height * Math.sin(2 * Math.PI * angle);
4815
+ var x = w / 2 + 0.4 * w * Math.cos(2 * Math.PI * angle);
4816
+ var y = h / 2 + 0.4 * h * Math.sin(2 * Math.PI * angle);
4213
4817
  agent.set({ x: x, y: y });
4214
4818
  }
4215
4819
  });
@@ -4254,8 +4858,8 @@
4254
4858
  context.globalAlpha = _this.opts.connectionOpacity;
4255
4859
  context.strokeStyle = _this.opts.connectionColor;
4256
4860
  context.lineWidth = _this.opts.connectionWidth;
4257
- context.moveTo(_this.x(x), _this.x(y));
4258
- context.lineTo(_this.x(nx), _this.x(ny));
4861
+ context.moveTo(_this.x(x), _this.y(y));
4862
+ context.lineTo(_this.x(nx), _this.y(ny));
4259
4863
  context.stroke();
4260
4864
  context.closePath();
4261
4865
  context.restore();
@@ -4287,10 +4891,11 @@
4287
4891
  }
4288
4892
  else if (shape === "triangle") {
4289
4893
  bufferContext.beginPath();
4894
+ var scaledSize = size * dpr;
4290
4895
  var points = [
4291
- [_this.x(x), _this.y(y) - size / 2],
4292
- [_this.x(x) + size / 2, _this.y(y) + size / 2],
4293
- [_this.x(x) - size / 2, _this.y(y) + size / 2]
4896
+ [_this.x(x), _this.y(y) - scaledSize / 2],
4897
+ [_this.x(x) + scaledSize / 2, _this.y(y) + scaledSize / 2],
4898
+ [_this.x(x) - scaledSize / 2, _this.y(y) + scaledSize / 2]
4294
4899
  ];
4295
4900
  _this.drawPath(points);
4296
4901
  if (environment.opts.torus)
@@ -4316,6 +4921,12 @@
4316
4921
  bufferContext.restore();
4317
4922
  }
4318
4923
  });
4924
+ // Draw selection highlights for selected agents
4925
+ if (opts.interactive && this.selected.length > 0) {
4926
+ this.selected.forEach(function (agent) {
4927
+ _this._drawSelectionHighlight(agent);
4928
+ });
4929
+ }
4319
4930
  context.drawImage(buffer, 0, 0);
4320
4931
  };
4321
4932
  return CanvasRenderer;
@@ -4406,11 +5017,13 @@
4406
5017
  };
4407
5018
  Histogram.prototype.x = function (value) {
4408
5019
  var _a = this, width = _a.width, markerWidth = _a.markerWidth;
4409
- return remap(value, 0, width, markerWidth + PADDING_AT_LEFT, width - PADDING_AT_RIGHT);
5020
+ var dpr = window.devicePixelRatio;
5021
+ return remap(value, 0, width, markerWidth + PADDING_AT_LEFT * dpr, width - PADDING_AT_RIGHT * dpr);
4410
5022
  };
4411
5023
  Histogram.prototype.y = function (value) {
4412
5024
  var _a = this, height = _a.height, maxValue = _a.maxValue;
4413
- return remap(value, 0, maxValue, height - PADDING_AT_BOTTOM, 0);
5025
+ var dpr = window.devicePixelRatio;
5026
+ return remap(value, 0, maxValue, height - PADDING_AT_BOTTOM * dpr, 0);
4414
5027
  };
4415
5028
  Histogram.prototype.setMaxValue = function () {
4416
5029
  var _this = this;
@@ -4439,11 +5052,12 @@
4439
5052
  var context = this.canvas.getContext("2d");
4440
5053
  var _a = this, height = _a.height, width = _a.width;
4441
5054
  var _b = this.opts, aboveMax = _b.aboveMax, belowMin = _b.belowMin, buckets = _b.buckets, min = _b.min, max$1 = _b.max;
5055
+ var dpr = window.devicePixelRatio;
4442
5056
  var yMin = 0;
4443
5057
  var yMax = this.maxValue;
4444
5058
  var markers = extractRoundNumbers({ min: yMin, max: yMax });
4445
5059
  context.fillStyle = "black";
4446
- context.font = 14 * window.devicePixelRatio + "px Helvetica";
5060
+ context.font = 14 * dpr + "px Helvetica";
4447
5061
  // determine the width of the longest marker
4448
5062
  this.markerWidth = max(markers.map(function (marker) { return context.measureText(marker.toLocaleString()).width; }));
4449
5063
  // draw horizontal lines
@@ -4452,9 +5066,9 @@
4452
5066
  context.textBaseline = "middle";
4453
5067
  context.fillText(marker.toLocaleString(), _this.markerWidth, _this.y(marker));
4454
5068
  context.beginPath();
4455
- context.moveTo(_this.markerWidth + 10, _this.y(marker));
5069
+ context.moveTo(_this.markerWidth + 10 * dpr, _this.y(marker));
4456
5070
  context.lineTo(_this.width, _this.y(marker));
4457
- context.setLineDash(LINE_DASH);
5071
+ context.setLineDash(LINE_DASH.map(function (v) { return v * dpr; }));
4458
5072
  context.stroke();
4459
5073
  });
4460
5074
  var numBuckets = bucketValues.length - (aboveMax ? 1 : 0) - (belowMin ? 1 : 0);
@@ -4477,9 +5091,9 @@
4477
5091
  .forEach(function (label, i) {
4478
5092
  context.save();
4479
5093
  context.translate(_this.x((i * width) / bucketValues.length +
4480
- (0.5 * width) / bucketValues.length), height - 50);
5094
+ (0.5 * width) / bucketValues.length), height - 50 * dpr);
4481
5095
  context.rotate(Math.PI / 4);
4482
- context.font = 12 * window.devicePixelRatio + "px Helvetica";
5096
+ context.font = 12 * dpr + "px Helvetica";
4483
5097
  context.textAlign = "left";
4484
5098
  context.textBaseline = "middle";
4485
5099
  context.fillText(label, 0, 0);
@@ -4489,22 +5103,23 @@
4489
5103
  Histogram.prototype.drawBuckets = function (bucketValues, offset) {
4490
5104
  var _this = this;
4491
5105
  if (offset === void 0) { offset = 0; }
4492
- var canvas = this.canvas;
5106
+ var _a = this, canvas = _a.canvas, width = _a.width, height = _a.height;
4493
5107
  var metric = this._metric;
4494
5108
  var numMetrics = Array.isArray(metric) ? metric.length : 1;
4495
- var _a = this.opts, aboveMax = _a.aboveMax, belowMin = _a.belowMin, color = _a.color, width = _a.width, height = _a.height;
5109
+ var _b = this.opts, aboveMax = _b.aboveMax, belowMin = _b.belowMin, color = _b.color;
5110
+ var dpr = window.devicePixelRatio;
4496
5111
  var context = canvas.getContext("2d");
4497
5112
  context.fillStyle = Array.isArray(color)
4498
5113
  ? color[offset % color.length]
4499
5114
  : color;
4500
5115
  var numBuckets = bucketValues.length;
4501
- var barWidth = (width - PADDING_AT_LEFT - PADDING_AT_RIGHT - this.markerWidth) /
5116
+ var barWidth = (width - PADDING_AT_LEFT * dpr - PADDING_AT_RIGHT * dpr - this.markerWidth) /
4502
5117
  numBuckets;
4503
5118
  barWidth *= 0.8;
4504
5119
  bucketValues.forEach(function (value, i) {
4505
5120
  var mappedValue = remap(value, 0, _this.maxValue, 0, 1);
4506
5121
  var x = _this.x(((0.1 + i) * width) / numBuckets);
4507
- context.fillRect(x + (offset * barWidth - (numMetrics - 1)) / numMetrics + offset, remap(mappedValue, 0, 1, height - PADDING_AT_BOTTOM, 0), barWidth / numMetrics, remap(mappedValue, 0, 1, 0, height - PADDING_AT_BOTTOM));
5122
+ context.fillRect(x + (offset * barWidth - (numMetrics - 1)) / numMetrics + offset, remap(mappedValue, 0, 1, height - PADDING_AT_BOTTOM * dpr, 0), barWidth / numMetrics, remap(mappedValue, 0, 1, 0, height - PADDING_AT_BOTTOM * dpr));
4508
5123
  });
4509
5124
  };
4510
5125
  Histogram.prototype.getBucketValues = function (metric) {
@@ -4641,12 +5256,16 @@
4641
5256
  var height = this.height;
4642
5257
  var range = this.opts.range;
4643
5258
  var min = range.min, max = range.max;
4644
- var pxPerUnit = (height - 2 * PADDING_BOTTOM) / (max - min);
4645
- return Math.round(height - (value - min) * pxPerUnit) - 2 * PADDING_BOTTOM;
5259
+ var dpr = window.devicePixelRatio;
5260
+ var paddingBottom = PADDING_BOTTOM * dpr;
5261
+ var pxPerUnit = (height - 2 * paddingBottom) / (max - min);
5262
+ return Math.round(height - (value - min) * pxPerUnit) - 2 * paddingBottom;
4646
5263
  };
4647
5264
  LineChartRenderer.prototype.drawBackground = function () {
4648
5265
  var _this = this;
4649
5266
  var _a = this, context = _a.context, width = _a.width, height = _a.height, opts = _a.opts, t = _a.t;
5267
+ var dpr = window.devicePixelRatio;
5268
+ var paddingBottom = PADDING_BOTTOM * dpr;
4650
5269
  // draw background and lines
4651
5270
  context.fillStyle = this.opts.background;
4652
5271
  context.fillRect(0, 0, width, height);
@@ -4654,27 +5273,27 @@
4654
5273
  var markers = extractRoundNumbers(range);
4655
5274
  var textMaxWidth = 0;
4656
5275
  // write values on vertical axis
4657
- context.font = 14 * window.devicePixelRatio + "px Helvetica";
5276
+ context.font = 14 * dpr + "px Helvetica";
4658
5277
  context.fillStyle = "#000";
4659
5278
  context.textBaseline = "middle";
4660
5279
  markers.forEach(function (marker) {
4661
- if (_this.y(marker) < 10 || _this.y(marker) + 10 > height)
5280
+ if (_this.y(marker) < 10 * dpr || _this.y(marker) + 10 * dpr > height)
4662
5281
  return;
4663
5282
  var width = context.measureText(marker.toLocaleString()).width;
4664
5283
  if (width > textMaxWidth)
4665
5284
  textMaxWidth = width;
4666
- context.fillText(marker.toLocaleString(), 5, _this.y(marker));
5285
+ context.fillText(marker.toLocaleString(), 5 * dpr, _this.y(marker));
4667
5286
  });
4668
5287
  // draw horizontal lines for vertical axis
4669
5288
  context.save();
4670
5289
  context.strokeStyle = "#999";
4671
5290
  markers.forEach(function (marker) {
4672
- if (_this.y(marker) >= height - PADDING_BOTTOM)
5291
+ if (_this.y(marker) >= height - paddingBottom)
4673
5292
  return;
4674
5293
  context.beginPath();
4675
- context.moveTo(textMaxWidth + 10, _this.y(marker));
5294
+ context.moveTo(textMaxWidth + 10 * dpr, _this.y(marker));
4676
5295
  context.lineTo(_this.x(Math.max(width, _this.environment.time)), _this.y(marker));
4677
- context.setLineDash(lineDash);
5296
+ context.setLineDash(lineDash.map(function (v) { return v * dpr; }));
4678
5297
  context.stroke();
4679
5298
  });
4680
5299
  context.restore();
@@ -4691,12 +5310,12 @@
4691
5310
  _this.x(marker) - width / 2 < textMaxWidth) {
4692
5311
  return;
4693
5312
  }
4694
- context.font = 11 * window.devicePixelRatio + "px Helvetica";
4695
- context.fillText(marker.toLocaleString(), _this.x(marker), height - PADDING_BOTTOM);
5313
+ context.font = 11 * dpr + "px Helvetica";
5314
+ context.fillText(marker.toLocaleString(), _this.x(marker), height - paddingBottom);
4696
5315
  context.strokeStyle = "black";
4697
5316
  context.lineWidth = 1;
4698
5317
  context.beginPath();
4699
- context.moveTo(_this.x(marker), height - 4);
5318
+ context.moveTo(_this.x(marker), height - 4 * dpr);
4700
5319
  context.lineTo(_this.x(marker), height);
4701
5320
  context.stroke();
4702
5321
  });
@@ -5044,7 +5663,8 @@
5044
5663
  */
5045
5664
  Heatmap.prototype.x = function (value) {
5046
5665
  var width = this.width;
5047
- return remap(value, this.getMin("x"), this.getMax("x"), PADDING_AT_LEFT$1, width);
5666
+ var dpr = window.devicePixelRatio;
5667
+ return remap(value, this.getMin("x"), this.getMax("x"), PADDING_AT_LEFT$1 * dpr, width);
5048
5668
  };
5049
5669
  /**
5050
5670
  * Map a value (on the range y-min to y-max) onto canvas space to draw it along the y-axis.
@@ -5052,7 +5672,8 @@
5052
5672
  */
5053
5673
  Heatmap.prototype.y = function (value) {
5054
5674
  var height = this.height;
5055
- return remap(value, this.getMin("y"), this.getMax("y"), height - PADDING_AT_BOTTOM$1, 0);
5675
+ var dpr = window.devicePixelRatio;
5676
+ return remap(value, this.getMin("y"), this.getMax("y"), height - PADDING_AT_BOTTOM$1 * dpr, 0);
5056
5677
  };
5057
5678
  /** @hidden */
5058
5679
  Heatmap.prototype.getKey = function (axis) {
@@ -5095,52 +5716,55 @@
5095
5716
  Heatmap.prototype.drawMarkers = function () {
5096
5717
  var _a = this, context = _a.context, width = _a.width, height = _a.height;
5097
5718
  var _b = this.opts, from = _b.from, to = _b.to;
5719
+ var dpr = window.devicePixelRatio;
5720
+ var padLeft = PADDING_AT_LEFT$1 * dpr;
5721
+ var padBottom = PADDING_AT_BOTTOM$1 * dpr;
5098
5722
  context.strokeStyle = "black";
5099
5723
  context.lineWidth = 1;
5100
- context.moveTo(PADDING_AT_LEFT$1 - 1, 0);
5101
- context.lineTo(PADDING_AT_LEFT$1 - 1, height - PADDING_AT_BOTTOM$1 + 1);
5102
- context.lineTo(width, height - PADDING_AT_BOTTOM$1 + 1);
5724
+ context.moveTo(padLeft - 1, 0);
5725
+ context.lineTo(padLeft - 1, height - padBottom + 1);
5726
+ context.lineTo(width, height - padBottom + 1);
5103
5727
  context.stroke();
5104
5728
  context.lineWidth = 0;
5105
- var gradient = context.createLinearGradient(10, 0, PADDING_AT_LEFT$1 - 10, 0);
5729
+ var gradient = context.createLinearGradient(10 * dpr, 0, padLeft - 10 * dpr, 0);
5106
5730
  gradient.addColorStop(0, from);
5107
5731
  gradient.addColorStop(1, to);
5108
5732
  context.fillStyle = gradient;
5109
- context.fillRect(10, height - PADDING_AT_BOTTOM$1 + 20, PADDING_AT_LEFT$1 - 24, 20);
5733
+ context.fillRect(10 * dpr, height - padBottom + 20 * dpr, padLeft - 24 * dpr, 20 * dpr);
5110
5734
  context.fillStyle = "black";
5111
5735
  var step = (this.getMax("x") - this.getMin("x")) / this.getBuckets("x");
5112
5736
  var originalStep = step;
5113
- while (Math.abs(this.x(step) - this.x(0)) < 35)
5737
+ while (Math.abs(this.x(step) - this.x(0)) < 35 * dpr)
5114
5738
  step *= 2;
5115
5739
  for (var marker = this.getMin("x"); marker <= this.getMax("x"); marker += originalStep) {
5116
- if (this.x(marker) + 10 > width)
5740
+ if (this.x(marker) + 10 * dpr > width)
5117
5741
  continue;
5118
- context.moveTo(this.x(marker), height - PADDING_AT_BOTTOM$1);
5119
- context.lineTo(this.x(marker), height - PADDING_AT_BOTTOM$1 + 10);
5742
+ context.moveTo(this.x(marker), height - padBottom);
5743
+ context.lineTo(this.x(marker), height - padBottom + 10 * dpr);
5120
5744
  context.stroke();
5121
5745
  if (Math.abs(((marker - this.getMin("x")) / step) % 1) < 0.001 ||
5122
5746
  Math.abs((((marker - this.getMin("x")) / step) % 1) - 1) < 0.001) {
5123
- context.font = 12 * window.devicePixelRatio + "px Helvetica";
5747
+ context.font = 12 * dpr + "px Helvetica";
5124
5748
  context.textAlign = "center";
5125
- context.fillText(marker.toLocaleString(), this.x(marker), height - PADDING_AT_BOTTOM$1 + 24);
5749
+ context.fillText(marker.toLocaleString(), this.x(marker), height - padBottom + 24 * dpr);
5126
5750
  }
5127
5751
  }
5128
5752
  step = (this.getMax("y") - this.getMin("y")) / this.getBuckets("y");
5129
5753
  originalStep = step;
5130
- while (Math.abs(this.y(step) - this.y(0)) < 20)
5754
+ while (Math.abs(this.y(step) - this.y(0)) < 20 * dpr)
5131
5755
  step *= 2;
5132
5756
  for (var marker = this.getMin("y"); marker <= this.getMax("y"); marker += originalStep) {
5133
- if (this.y(marker) - 10 < 0)
5757
+ if (this.y(marker) - 10 * dpr < 0)
5134
5758
  continue;
5135
- context.moveTo(PADDING_AT_LEFT$1, this.y(marker));
5136
- context.lineTo(PADDING_AT_LEFT$1 - 10, this.y(marker));
5759
+ context.moveTo(padLeft, this.y(marker));
5760
+ context.lineTo(padLeft - 10 * dpr, this.y(marker));
5137
5761
  context.stroke();
5138
5762
  if (Math.abs(((marker - this.getMin("y")) / step) % 1) < 0.001 ||
5139
5763
  Math.abs((((marker - this.getMin("y")) / step) % 1) - 1) < 0.001) {
5140
- context.font = 12 * window.devicePixelRatio + "px Helvetica";
5764
+ context.font = 12 * dpr + "px Helvetica";
5141
5765
  context.textAlign = "right";
5142
5766
  context.textBaseline = "middle";
5143
- context.fillText(marker.toLocaleString(), PADDING_AT_LEFT$1 - 14, this.y(marker));
5767
+ context.fillText(marker.toLocaleString(), padLeft - 14 * dpr, this.y(marker));
5144
5768
  }
5145
5769
  }
5146
5770
  };
@@ -5148,6 +5772,8 @@
5148
5772
  Heatmap.prototype.updateScale = function () {
5149
5773
  var _a = this, context = _a.context, environment = _a.environment, height = _a.height;
5150
5774
  var scale = this.opts.scale;
5775
+ var dpr = window.devicePixelRatio;
5776
+ var padLeft = PADDING_AT_LEFT$1 * dpr;
5151
5777
  var max = scale === "relative" ? this.localMax : this.opts.max;
5152
5778
  if (max === undefined) {
5153
5779
  if (!this.lastUpdatedScale) {
@@ -5156,13 +5782,13 @@
5156
5782
  max = environment.getAgents().length;
5157
5783
  }
5158
5784
  if (!this.lastUpdatedScale || +new Date() - +this.lastUpdatedScale > 250) {
5159
- context.clearRect(0, height - 20, PADDING_AT_LEFT$1, 20);
5785
+ context.clearRect(0, height - 20 * dpr, padLeft, 20 * dpr);
5160
5786
  context.fillStyle = "black";
5161
- context.font = 12 * window.devicePixelRatio + "px Helvetica";
5787
+ context.font = 12 * dpr + "px Helvetica";
5162
5788
  context.textAlign = "center";
5163
5789
  context.textBaseline = "bottom";
5164
- context.fillText("0", 10, height - 5);
5165
- context.fillText(max.toString(), PADDING_AT_LEFT$1 - 16, height - 5);
5790
+ context.fillText("0", 10 * dpr, height - 5 * dpr);
5791
+ context.fillText(max.toString(), padLeft - 16 * dpr, height - 5 * dpr);
5166
5792
  this.lastUpdatedScale = new Date();
5167
5793
  }
5168
5794
  };
@@ -5170,6 +5796,9 @@
5170
5796
  Heatmap.prototype.drawRectangles = function () {
5171
5797
  var _a = this, canvas = _a.canvas, environment = _a.environment, width = _a.width, height = _a.height;
5172
5798
  var _b = this.opts, scale = _b.scale, from = _b.from, to = _b.to;
5799
+ var dpr = window.devicePixelRatio;
5800
+ var padLeft = PADDING_AT_LEFT$1 * dpr;
5801
+ var padBottom = PADDING_AT_BOTTOM$1 * dpr;
5173
5802
  var context = canvas.getContext("2d");
5174
5803
  var xBuckets = this.getBuckets("x");
5175
5804
  var yBuckets = this.getBuckets("y");
@@ -5178,9 +5807,9 @@
5178
5807
  max = environment.getAgents().length;
5179
5808
  // clear background by drawing background rectangle
5180
5809
  context.fillStyle = from;
5181
- context.fillRect(PADDING_AT_LEFT$1, 0, width, height - PADDING_AT_BOTTOM$1);
5182
- var w = width / xBuckets;
5183
- var h = height / yBuckets;
5810
+ context.fillRect(padLeft, 0, width - padLeft, height - padBottom);
5811
+ var w = (width - padLeft) / xBuckets;
5812
+ var h = (height - padBottom) / yBuckets;
5184
5813
  for (var row = 0; row < yBuckets; row++) {
5185
5814
  for (var column = 0; column < xBuckets; column++) {
5186
5815
  var index = row * xBuckets + column;
@@ -5188,7 +5817,7 @@
5188
5817
  var a = clamp(remap(this.buckets[index], 0, max, 0, 1), 0, 1);
5189
5818
  context.fillStyle = to;
5190
5819
  context.globalAlpha = a;
5191
- context.fillRect(this.x(remap(column, 0, xBuckets, this.getMin("x"), this.getMax("x"))), this.y(remap(row, -1, yBuckets - 1, this.getMin("y"), this.getMax("y"))), (w * (width - PADDING_AT_LEFT$1)) / width, (h * (height - PADDING_AT_BOTTOM$1)) / height);
5820
+ context.fillRect(this.x(remap(column, 0, xBuckets, this.getMin("x"), this.getMax("x"))), this.y(remap(row, -1, yBuckets - 1, this.getMin("y"), this.getMax("y"))), w, h);
5192
5821
  }
5193
5822
  }
5194
5823
  context.globalAlpha = 1;
@@ -5244,7 +5873,7 @@
5244
5873
  /**
5245
5874
  * The current version of the Flocc library.
5246
5875
  */
5247
- var version = "0.5.21";
5876
+ var version = "0.5.23";
5248
5877
 
5249
5878
  exports.ASCIIRenderer = ASCIIRenderer;
5250
5879
  exports.Agent = Agent;