• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wavedrom / bitfield / 4285786967

pending completion
4285786967

push

github

GitHub
Merge pull request #52 from amiq-eda/fix_legend_spacing

89 of 220 branches covered (40.45%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

157 of 263 relevant lines covered (59.7%)

12.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.73
/lib/render.js
1
'use strict';
2

3
const tspan = require('tspan');
1✔
4

5
// ----- ✂ ------------------------------------------------------------
6

7
const round = Math.round;
1✔
8

9
const getSVG = (w, h) => ['svg', {
2✔
10
  xmlns: 'http://www.w3.org/2000/svg',
11
  // TODO link ns?
12
  width: w,
13
  height: h,
14
  viewBox: [0, 0, w, h].join(' ')
15
}];
16

17
const tt = (x, y, obj) => Object.assign(
38✔
18
  {transform: 'translate(' + x + (y ? (',' + y) : '') + ')'},
38✔
19
  (typeof obj === 'object') ? obj : {}
38✔
20
);
21

22
const colors = { // TODO compare with WaveDrom
1✔
23
  2: 0,
24
  3: 80,
25
  4: 170,
26
  5: 45,
27
  6: 126,
28
  7: 215
29
};
30

31
const typeStyle = t => (colors[t] !== undefined)
3✔
32
  ? ';fill:hsl(' + colors[t] + ',100%,50%)'
33
  : '';
34

35
const norm = (obj, other) => Object.assign(
129✔
36
  Object
37
    .keys(obj)
38
    .reduce((prev, key) => {
39
      const val = Number(obj[key]);
513✔
40
      const valInt = isNaN(val) ? 0 : Math.round(val);
513!
41
      if (valInt !== 0) { prev[key] = valInt; }
513✔
42
      return prev;
513✔
43
    }, {}),
44
  other
45
);
46

47
const trimText = (text, availableSpace, charWidth) => {
1✔
48
  if (!(typeof text === 'string' || text instanceof String))
×
49
    return text;
×
50

51
  const textWidth = text.length * charWidth;
×
52
  if (textWidth <= availableSpace)
×
53
    return text;
×
54

55
  var end = text.length - ((textWidth - availableSpace) / charWidth) - 3;
×
56
  if (end > 0)
×
57
    return text.substring(0, round(end)) + '...';
×
58
  return text.substring(0, 1) + '...';
×
59
};
60

61
const text = (body, x, y, rotate) => {
1✔
62
  const props = {y: 6};
28✔
63
  if (rotate !== undefined) {
28✔
64
    props.transform = 'rotate(' + rotate + ')';
1✔
65
  }
66
  return ['g', tt(round(x), round(y)), ['text', props].concat(tspan.parse(body))];
28✔
67
};
68

69
const hline = (len, x, y) => ['line', norm({x1: x, x2: x + len, y1: y, y2: y})];
4✔
70
const vline = (len, x, y) => ['line', norm({x1: x, x2: x, y1: y, y2: y + len})];
122✔
71

72
const getLabel = (val, x, y, step, len, rotate) => {
1✔
73
  if (typeof val !== 'number') {
10✔
74
    return text(val, x, y, rotate);
9✔
75
  }
76
  const res = ['g', {}];
1✔
77
  for (let i = 0; i < len; i++) {
1✔
78
    res.push(text(
3✔
79
      (val >> i) & 1,
80
      x + step * (len / 2 - i - 0.5),
81
      y
82
    ));
83
  }
84
  return res;
1✔
85
};
86

87
const getAttr = (e, opt, step, lsbm, msbm) => {
1✔
88
  const x = opt.vflip
4!
89
    ? step * ((msbm + lsbm) / 2)
90
    : step * (opt.mod - ((msbm + lsbm) / 2) - 1);
91

92
  if (!Array.isArray(e.attr)) {
4!
93
    return getLabel(e.attr, x, 0, step, e.bits);
4✔
94
  }
95
  return e.attr.reduce((prev, a, i) =>
×
96
    (a === undefined || a === null)
×
97
      ? prev
98
      : prev.concat([getLabel(a, x, opt.fontsize * i, step, e.bits)]),
99
  ['g', {}]);
100
};
101

102
const labelArr = (desc, opt) => {
1✔
103
  const {margin, hspace, vspace, mod, index, fontsize, vflip, trim, compact, offset} = opt;
2✔
104
  const width = hspace - margin.left - margin.right - 1;
2✔
105
  const height = vspace - margin.top - margin.bottom;
2✔
106
  const step = width / mod;
2✔
107
  const blanks = ['g'];
2✔
108
  const bits = ['g', tt(round(step / 2), -round(0.5 * fontsize + 4))];
2✔
109
  const names = ['g', tt(round(step / 2), round(0.5 * height + 0.4 * fontsize - 6))];
2✔
110
  const attrs = ['g', tt(round(step / 2), round(height + 0.7 * fontsize - 2))];
2✔
111
  desc.map(e => {
2✔
112
    let lsbm = 0;
8✔
113
    let msbm = mod - 1;
8✔
114
    let lsb = index * mod;
8✔
115
    let msb = (index + 1) * mod - 1;
8✔
116
    if (((e.lsb / mod) >> 0) === index) {
8!
117
      lsbm = e.lsbm;
8✔
118
      lsb = e.lsb;
8✔
119
      if (((e.msb / mod) >> 0) === index) {
8!
120
        msb = e.msb;
8✔
121
        msbm = e.msbm;
8✔
122
      }
123
    } else {
124
      if (((e.msb / mod) >> 0) === index) {
×
125
        msb = e.msb;
×
126
        msbm = e.msbm;
×
127
      } else if (!(lsb > e.lsb && msb < e.msb)) {
×
128
        return;
×
129
      }
130
    }
131
    if (!compact) {
8!
132
      bits.push(text(lsb + offset, step * (vflip ? lsbm : (mod - lsbm - 1))));
8!
133
      if (lsbm !== msbm) {
8!
134
        bits.push(text(msb + offset, step * (vflip ? msbm : (mod - msbm - 1))));
8!
135
      }
136
    }
137
    if (e.name !== undefined) {
8✔
138
      names.push(getLabel(
6✔
139
        trim ? trimText(e.name, step * e.bits, trim) : e.name,
6!
140
        step * (vflip
6!
141
          ? ((msbm + lsbm) / 2)
142
          : (mod - ((msbm + lsbm) / 2) - 1)
143
        ),
144
        0,
145
        step,
146
        e.bits,
147
        e.rotate
148
      ));
149
    }
150

151
    if ((e.name === undefined) || (e.type !== undefined)) {
8✔
152
      if (!(opt.compact && e.type === undefined)) {
3!
153
        blanks.push(['rect', norm({
3✔
154
          x: step * (vflip ? lsbm : (mod - msbm - 1)),
3!
155
          width: step * (msbm - lsbm + 1),
156
          height: height
157
        }, {
158
          field: e.name,
159
          style: 'fill-opacity:0.1' + typeStyle(e.type)
160
        })]);
161
      }
162
    }
163
    if (e.attr !== undefined) {
8✔
164
      attrs.push(getAttr(e, opt, step, lsbm, msbm));
4✔
165
    }
166
  });
167
  return ['g', blanks, bits, names, attrs];
2✔
168
};
169

170
const getLabelMask = (desc, mod) => {
1✔
171
  const mask = [];
×
172
  let idx = 0;
×
173
  desc.map(e => {
×
174
    mask[idx % mod] = true;
×
175
    idx += e.bits;
×
176
    mask[(idx - 1) % mod] = true;
×
177
  });
178
  return mask;
×
179
};
180

181
const getLegendItems = (opt) => {
1✔
182
  const {hspace, margin, fontsize, legend} = opt;
×
183
  const width = hspace - margin.left - margin.right - 1;
×
184
  const items = ['g', tt(margin.left, -10)];
×
185
  const legendSquarePadding = 36;
×
186
  const legendNamePadding = 24;
×
187

188
  let x = width / 2 - Object.keys(legend).length / 2 * (legendSquarePadding + legendNamePadding);
×
189
  for(const key in legend) {
×
190
    const value = legend[key];
×
191

192
    items.push(['rect', norm({
×
193
      x: x,
194
      width: 12,
195
      height: 12
196
    }, {
197
      style: 'fill-opacity:0.15; stroke: #000; stroke-width: 1.2;' + typeStyle(value)
198
    })]);
199

200
    x += legendSquarePadding;
×
201
    items.push(text(
×
202
      key,
203
      x,
204
      0.1 * fontsize + 4
205
    ));
206
    x += legendNamePadding;
×
207
  }
208

209
  return items;
×
210
};
211

212
const compactLabels = (desc, opt) => {
1✔
213
  const {hspace, margin, mod, fontsize, vflip, legend, offset} = opt;
×
214
  const width = hspace - margin.left - margin.right - 1;
×
215
  const step = width / mod;
×
216
  const labels = ['g', tt(margin.left, legend ? 0 : -3)];
×
217

218
  const mask = getLabelMask(desc, mod);
×
219

220
  for (let i = 0; i < mod; i++) {
×
221
    const idx = vflip ? i : (mod - i - 1);
×
222
    if (mask[idx]) {
×
223
      labels.push(text(
×
224
        idx + offset,
225
        step * (i + .5),
226
        0.5 * fontsize + 4
227
      ));
228
    }
229
  }
230

231
  return labels;
×
232
};
233

234
const skipDrawingEmptySpace = (desc, opt, laneIndex, laneLength, globalIndex) => {
1✔
235
  if (!opt.compact)
×
236
    return false;
×
237

238
  const isEmptyBitfield = (bitfield) => bitfield.name === undefined && bitfield.type === undefined;
×
239
  const bitfieldIndex = desc.findIndex((e) => isEmptyBitfield(e) && globalIndex >= e.lsb && globalIndex <= e.msb + 1);
×
240

241
  if (bitfieldIndex === -1) {
×
242
    return false;
×
243
  }
244

245
  if (globalIndex > desc[bitfieldIndex].lsb && globalIndex < desc[bitfieldIndex].msb + 1) {
×
246
    return true;
×
247
  }
248

249
  if (globalIndex == desc[bitfieldIndex].lsb && (laneIndex === 0 || bitfieldIndex > 0 && isEmptyBitfield(desc[bitfieldIndex - 1]))) {
×
250
    return true;
×
251
  }
252

253
  if (globalIndex == desc[bitfieldIndex].msb + 1 && (laneIndex === laneLength || bitfieldIndex < desc.length - 1 && isEmptyBitfield(desc[bitfieldIndex + 1]))) {
×
254
    return true;
×
255
  }
256

257
  return false;
×
258
};
259

260
const cage = (desc, opt) => {
1✔
261
  const {hspace, vspace, mod, margin, index, vflip} = opt;
2✔
262
  const width = hspace - margin.left - margin.right - 1;
2✔
263
  const height = vspace - margin.top - margin.bottom;
2✔
264
  const res = ['g',
2✔
265
    {
266
      stroke: 'black',
267
      'stroke-width': 1,
268
      'stroke-linecap': 'round'
269
    }
270
  ];
271
  if (opt.sparse) {
2!
272
    const skipEdge = opt.uneven && (opt.bits % 2 === 1) && (index === (opt.lanes - 1));
×
273
    if (skipEdge) {
×
274
      if (vflip) {
×
275
        res.push(
×
276
          hline(width - (width / mod), 0, 0),
277
          hline(width - (width / mod), 0, height)
278
        );
279
      } else {
280
        res.push(
×
281
          hline(width - (width / mod), width / mod, 0),
282
          hline(width - (width / mod), width / mod, height)
283
        );
284
      }
285
    } else if (!opt.compact) {
×
286
      res.push(
×
287
        hline(width, 0, 0),
288
        hline(width, 0, height),
289
        vline(height, (vflip ? width : 0), 0)
×
290
      );
291
    }
292
  } else {
293
    res.push(
2✔
294
      hline(width, 0, 0),
295
      vline(height, (vflip ? width : 0), 0),
2!
296
      hline(width, 0, height)
297
    );
298
  }
299

300
  let i = index * mod;
2✔
301
  const delta = vflip ? 1 : -1;
2!
302
  let j = vflip ? 0 : mod;
2!
303

304
  if (opt.sparse) {
2!
305
    for (let k = 0; k <= mod; k++) {
×
306
      if (skipDrawingEmptySpace(desc, opt, k, mod, i)) {
×
307
        i++;
×
308
        j += delta;
×
309
        continue;
×
310
      }
311
      const xj = j * (width / mod);
×
312
      if ((k === 0) || (k === mod) || desc.some(e => (e.msb + 1 === i))) {
×
313
        res.push(vline(height, xj, 0));
×
314
      } else {
315
        res.push(
×
316
          vline((height >>> 3), xj, 0),
317
          vline(-(height >>> 3), xj, height)
318
        );
319
      }
320
      if (opt.compact && k !== 0 && !skipDrawingEmptySpace(desc, opt, k - 1, mod, i - 1)) {
×
321
        res.push(
×
322
          hline(width / mod, xj, 0),
323
          hline(width / mod, xj, height)
324
        );
325
      }
326
      i++;
×
327
      j += delta;
×
328
    }
329
  } else {
330
    for (let k = 0; k < mod; k++) {
2✔
331
      const xj = j * (width / mod);
64✔
332
      if ((k === 0) || desc.some(e => (e.lsb === i))) {
233✔
333
        res.push(vline(height, xj, 0));
8✔
334
      } else {
335
        res.push(
56✔
336
          vline((height >>> 3), xj, 0),
337
          vline(-(height >>> 3), xj, height)
338
        );
339
      }
340
      i++;
64✔
341
      j += delta;
64✔
342
    }
343
  }
344
  return res;
2✔
345
} /* eslint complexity: [1, 23] */;
346

347
const lane = (desc, opt) => {
1✔
348
  const {index, vspace, hspace, margin, hflip, lanes, compact, label} = opt;
2✔
349
  const height = vspace - margin.top - margin.bottom;
2✔
350
  const width = hspace - margin.left - margin.right - 1;
2✔
351

352
  let tx = margin.left;
2✔
353
  const idx = hflip ? index : (lanes - index - 1);
2!
354
  let ty = round(idx * vspace + margin.top);
2✔
355
  if (compact) {
2!
356
    ty = round(idx * height + margin.top);
×
357
  }
358
  const res = ['g',
2✔
359
    tt(tx, ty),
360
    cage(desc, opt),
361
    labelArr(desc, opt)
362
  ];
363

364
  if (label && label.left !== undefined) {
2!
365
    const lab = label.left;
×
366
    let txt = index;
×
367
    if (typeof lab === 'string') {
×
368
      txt = lab;
×
369
    } else
370
    if (typeof lab === 'number') {
×
371
      txt += lab;
×
372
    } else
373
    if (typeof lab === 'object') {
×
374
      txt = lab[index] || txt;
×
375
    }
376
    res.push(['g', {'text-anchor': 'end'},
×
377
      text(txt, -4, round(height / 2))
378
    ]);
379
  }
380

381
  if (label && label.right !== undefined) {
2!
382
    const lab = label.right;
×
383
    let txt = index;
×
384
    if (typeof lab === 'string') {
×
385
      txt = lab;
×
386
    } else
387
    if (typeof lab === 'number') {
×
388
      txt += lab;
×
389
    } else
390
    if (typeof lab === 'object') {
×
391
      txt = lab[index] || txt;
×
392
    }
393
    res.push(['g', {'text-anchor': 'start'},
×
394
      text(txt, width + 4, round(height / 2))
395
    ]);
396
  }
397

398
  return res;
2✔
399
};
400

401
// Maximum number of attributes across all fields
402
const getMaxAttributes = desc =>
1✔
403
  desc.reduce((prev, field) =>
2✔
404
    Math.max(
8✔
405
      prev,
406
      (field.attr === undefined)
8✔
407
        ? 0
408
        : Array.isArray(field.attr)
4!
409
          ? field.attr.length
410
          : 1
411
    ),
412
  0);
413

414
const getTotalBits = desc =>
1✔
415
  desc.reduce((prev, field) => prev + ((field.bits === undefined) ? 0 : field.bits), 0);
8!
416

417
const isIntGTorDefault = opt => row => {
2✔
418
  const [key, min, def] = row;
8✔
419
  const val = Math.round(opt[key]);
8✔
420
  opt[key] = (typeof val === 'number' && val >= min) ? val : def;
8✔
421
};
422

423
const optDefaults = opt => {
1✔
424
  opt = (typeof opt === 'object') ? opt : {};
2✔
425

426
  [ // key         min default
2✔
427
    // ['vspace', 20, 60],
428
    ['hspace', 40, 800],
429
    ['lanes', 1, 1],
430
    ['bits', 1, undefined],
431
    ['fontsize', 6, 14]
432
  ].map(isIntGTorDefault(opt));
433

434
  opt.fontfamily = opt.fontfamily || 'sans-serif';
2✔
435
  opt.fontweight = opt.fontweight || 'normal';
2✔
436
  opt.compact = opt.compact || false;
2✔
437
  opt.hflip = opt.hflip || false;
2✔
438
  opt.uneven = opt.uneven || false;
2✔
439
  opt.margin = opt.margin || {};
2✔
440
  opt.offset = opt.offset || 0;
2✔
441

442
  return opt;
2✔
443
};
444

445
const render = (desc, opt) => {
1✔
446
  opt = optDefaults(opt);
2✔
447

448
  const maxAttributes = getMaxAttributes(desc);
2✔
449

450
  opt.vspace = opt.vspace || ((maxAttributes + 4) * opt.fontsize);
2✔
451

452
  if (opt.bits === undefined) {
2!
453
    opt.bits = getTotalBits(desc);
2✔
454
  }
455

456
  const {hspace, vspace, lanes, margin, compact, fontsize, bits, label, legend} = opt;
2✔
457

458
  if (margin.right === undefined) {
2!
459
    if (label && label.right !== undefined) {
2!
460
      margin.right = round(.1 * hspace);
×
461
    } else {
462
      margin.right = 4;
2✔
463
    }
464
  }
465

466
  if (margin.left === undefined) {
2!
467
    if (label && label.left !== undefined) {
2!
468
      margin.left = round(.1 * hspace);
×
469
    } else {
470
      margin.left = 4; // margin.right;
2✔
471
    }
472
  }
473
  if (margin.top === undefined) {
2!
474
    margin.top = 1.5 * fontsize;
2✔
475
    if (margin.bottom === undefined) {
2!
476
      margin.bottom = fontsize * (maxAttributes) + 4;
2✔
477
    }
478
  } else {
479
    if (margin.bottom === undefined) {
×
480
      margin.bottom = 4;
×
481
    }
482
  }
483

484
  const width = hspace;
2✔
485
  let height = vspace * lanes;
2✔
486
  if (compact) {
2!
487
    height -= (lanes - 1) * (margin.top + margin.bottom);
×
488
  }
489

490
  if (legend) {
2!
491
    height += 12;
×
492
  }
493

494
  const res = ['g',
2✔
495
    tt(0.5, legend ? 12.5 : 0.5, {
2!
496
      'text-anchor': 'middle',
497
      'font-size': opt.fontsize,
498
      'font-family': opt.fontfamily,
499
      'font-weight': opt.fontweight
500
    })
501
  ];
502

503
  let lsb = 0;
2✔
504
  const mod = Math.ceil(bits * 1.0 / lanes);
2✔
505
  opt.mod = mod | 0;
2✔
506

507
  desc.map(e => {
2✔
508
    e.lsb = lsb;
8✔
509
    e.lsbm = lsb % mod;
8✔
510
    lsb += e.bits;
8✔
511
    e.msb = lsb - 1;
8✔
512
    e.msbm = e.msb % mod;
8✔
513
  });
514

515
  for (let i = 0; i < lanes; i++) {
2✔
516
    opt.index = i;
2✔
517
    res.push(lane(desc, opt));
2✔
518
  }
519
  if (compact) {
2!
520
    res.push(compactLabels(desc, opt));
×
521
  }
522

523
  if (legend) {
2!
524
    res.push(getLegendItems(opt));
×
525
  }
526

527
  return getSVG(width, height).concat([res]);
2✔
528
};
529

530
// ----- ✂ ------------------------------------------------------------
531

532
module.exports = render;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc