This demo uses imperative chart updates. The generated code is a faithful Angular translation: open it in CodeSandbox to run and tweak.

import { Component, AfterViewInit, OnDestroy, ViewChild } from '@angular/core';
import {
  ChartComponent,
  ApexAxisChartSeries,
  ApexNonAxisChartSeries,
  ApexChart,
  ApexXAxis,
  ApexYAxis,
  ApexTitleSubtitle,
  ApexDataLabels,
  ApexStroke,
  ApexFill,
  ApexLegend,
  ApexTooltip,
  ApexMarkers,
  ApexPlotOptions,
  ApexResponsive,
  ApexGrid,
  ApexAnnotations,
  ApexStates,
  ApexTheme,
  NgApexchartsModule,
} from 'ng-apexcharts';

export type ChartOptions = {
  series?: ApexAxisChartSeries | ApexNonAxisChartSeries;
  chart?: ApexChart;
  xaxis?: ApexXAxis;
  yaxis?: ApexYAxis | ApexYAxis[];
  title?: ApexTitleSubtitle;
  subtitle?: ApexTitleSubtitle;
  dataLabels?: ApexDataLabels;
  stroke?: ApexStroke;
  fill?: ApexFill;
  legend?: ApexLegend;
  tooltip?: ApexTooltip;
  markers?: ApexMarkers;
  plotOptions?: ApexPlotOptions;
  responsive?: ApexResponsive[];
  grid?: ApexGrid;
  annotations?: ApexAnnotations;
  states?: ApexStates;
  theme?: ApexTheme;
  colors?: string[];
  labels?: any;
};

@Component({
  selector: 'app-chart',
  standalone: true,
  imports: [NgApexchartsModule],
  templateUrl: './chart.component.html',
})
export class AppChart implements AfterViewInit, OnDestroy {
  @ViewChild('chart') chart!: ChartComponent;
  private SAMPLE_BUDGET: any = 2600;

  private svgOutline = (markup: any): any => {
        var host = document.createElement('div')
        // Rendered but off-screen: getCTM() and getPointAtLength() need a live layout,
        // and `display:none` would return null matrices.
        host.setAttribute(
          'style',
          'position:absolute;left:-99999px;top:0;width:1000px;height:1000px;overflow:hidden',
        )
        host.innerHTML = markup
        document.body.appendChild(host)
        try {
          var svg = host.querySelector('svg')
          if (!svg) return { error: 'No <svg> element found in that file.' }
  
          var nodes = svg.querySelectorAll(
            'path,circle,ellipse,rect,polygon,polyline,line',
          )
          var geo = []
          for (var i = 0; i < nodes.length; i++) {
            var el = nodes[i]
            if (typeof el.getTotalLength !== 'function') continue
            var len = 0
            try {
              len = el.getTotalLength()
            } catch (e) {
              continue // a degenerate element (zero-radius circle, empty d)
            }
            if (!(len > 0)) continue
            var cs = window.getComputedStyle(el)
            if (cs.display === 'none' || Number(cs.opacity) === 0) continue
            var fill = cs.fill
            geo.push({
              el: el,
              len: len,
              filled: !!fill && fill !== 'none' && fill !== 'transparent',
              // A real logo file usually SAYS which rule it wants. Reading it beats
              // making someone guess with the toggle.
              rule: cs.fillRule === 'evenodd' ? 'evenodd' : 'nonzero',
              strokeWidth: parseFloat(cs.strokeWidth) || 0,
            })
          }
          if (!geo.length) return { error: 'That SVG has no drawable shapes in it.' }
  
          // A logo is either filled artwork or a monoline drawn in strokes. If
          // anything is filled, the filled parts ARE the mark and the strokes are
          // trim; if nothing is, the strokes are the mark and their centrelines go to
          // `strokeFrom` instead.
          var filled = geo.filter(function (g) {
            return g.filled
          })
          var use = filled.length ? filled : geo
          var stroked = !filled.length
          var totalLen = use.reduce(function (s, g) {
            return s + g.len
          }, 0)
  
          var parts = []
          var widths = []
          use.forEach(function (g) {
            // Sample each element in proportion to its length, so a long swooping
            // curve gets the points and a tiny dot does not.
            var n = Math.max(10, Math.round((g.len / totalLen) * this.SAMPLE_BUDGET))
            var step = g.len / n
            var m = g.el.getCTM()
            var k2 = m ? Math.sqrt(Math.abs(m.a * m.d - m.b * m.c)) || 1 : 1
  
            // One `d` can hold several subpaths (the ring and its counter, a dotted
            // i, every letter of an outlined wordmark), and `getPointAtLength` walks
            // them end to end without saying where one stops. Sampling straight
            // through therefore draws a chord ACROSS the gap, and the packer, which
            // cannot tell that edge from a real one, fills a spoke of dots along it.
            //
            // It does not need to be told: arc length does not accrue across the
            // gap, so the jump appears as a chord far longer than the sampling step.
            // For any real curve the chord between two samples is at MOST the arc
            // length between them, so anything past a few steps is a subpath
            // boundary and starts a new polygon. Distances are compared after the
            // transform, so the step is scaled by the matrix too.
            var jump = step * k2 * 3 + 1e-6
            var poly = []
            var prev = null
  
            var flush = function () {
              // Two points cannot bound an area, and a stray one is noise.
              if (poly.length < 3) return
              var d = ''
              poly.forEach(function (p, i) {
                d += (i === 0 ? 'M ' : ' L ') + this.round2(p.x) + ' ' + this.round2(p.y)
              })
              // Close a filled subpath; leave a centreline open, or the stroke
              // doubles back on itself from finish to start.
              parts.push(stroked ? d : d + ' Z')
            }
  
            for (var k = 0; k <= n; k++) {
              var p = g.el.getPointAtLength((k / n) * g.len)
              if (m) p = p.matrixTransform(m)
              if (prev && Math.hypot(p.x - prev.x, p.y - prev.y) > jump) {
                flush()
                poly = []
              }
              poly.push({ x: p.x, y: p.y })
              prev = p
            }
            flush()
  
            if (stroked && g.strokeWidth) {
              // The stroke width has to travel into the same space as the points.
              widths.push(g.strokeWidth * k2)
            }
          })
          if (!parts.length)
            return { error: 'That SVG has no drawable shapes in it.' }
  
          return {
            path: parts.join(' '),
            stroked: stroked,
            width: widths.length ? this.median(widths) : 8,
            // Elements found vs polygons recovered: one <path> holding a ring and its
            // counter is 1 element and 2 subpaths, and it is the second number that
            // decides whether the mark has a hole in it.
            elements: use.length,
            subpaths: parts.length,
            // The rule the artwork itself declares, for `apply` to adopt on load.
            rule: use[0].rule,
            skipped: geo.length - use.length,
          }
        } catch (e) {
          return { error: 'That file could not be read as SVG (' + e.message + ').' }
        } finally {
          document.body.removeChild(host)
        }
      };

  private round2 = (v: any): any => {
        return Math.round(v * 100) / 100
      };

  private median = (a: any): any => {
        var s = a.slice().sort(function (x, y) {
          return x - y
        })
        return s[Math.floor(s.length / 2)]
      };

  private PRESETS: any = {
        // Halo's own mark (the storyboard sample's brand): eight overlapping subpaths
        // under the DEFAULT nonzero rule, where the handle's finger hole is the loop's
        // ellipse wound backwards. Try the fillRule toggle on this one: even-odd
        // treats every overlap as a hole and takes the cup apart.
        Cup:
          '<svg viewBox="0 0 100 100"><path fill="#5C3A21" d="' +
          'M 72 42 L 62 72 C 62 76, 55 77, 46 77 C 37 77, 30 76, 30 72 L 20 42 Z ' +
          'M 20 42 A 26 7 0 0 1 72 42 A 26 7 0 0 1 20 42 Z ' +
          'M 38 75 L 54 75 L 57 81.5 L 35 81.5 Z ' +
          'M 4 86.5 A 42 6.5 0 0 1 88 86.5 A 42 6.5 0 0 1 4 86.5 Z ' +
          'M 64 58 A 14 11 0 0 1 92 58 A 14 11 0 0 1 64 58 Z ' +
          'M 74 58 A 7 5 0 0 0 88 58 A 7 5 0 0 0 74 58 Z ' +
          'M 48 37 C 36 27, 56 19, 47 5 C 50 4, 52 5, 54 7 C 60 19, 44 26, 54 37 ' +
          'C 52 38, 50 38, 48 37 Z ' +
          'M 30 36 C 21 28, 35 21, 28 9 C 30 8, 32 9, 34 11 C 39 21, 28 27, 36 36 ' +
          'C 34 37, 32 37, 30 36 Z' +
          '" /></svg>',
        // Three NESTED circles, which is the case even-odd exists for. Under nonzero
        // they are all wound the same way, so they union into a plain disc: the
        // counter and the bean vanish. That is the single most common reason a pasted
        // logo comes out wrong, and the toggle is the fix.
        'Ring (evenodd)':
          '<svg viewBox="0 0 100 100">' +
          '<path fill="#5C3A21" fill-rule="evenodd" d="' +
          'M 4 50 A 46 46 0 1 1 96 50 A 46 46 0 1 1 4 50 Z ' +
          'M 20 50 A 30 30 0 1 1 80 50 A 30 30 0 1 1 20 50 Z ' +
          'M 34.44 60.9 A 19 12 -35 1 1 65.56 39.1 A 19 12 -35 1 1 34.44 60.9 Z' +
          '" /></svg>',
        // No fill anywhere, so this one arrives as a centreline plus a width.
        Current:
          '<svg viewBox="0 0 100 100">' +
          '<g transform="translate(2 4) rotate(-4 50 50)">' +
          '<path fill="none" stroke="#2E8B9A" stroke-width="9" ' +
          'd="M 6 62 C 26 26, 42 82, 58 46 S 78 20, 92 40" />' +
          '<path fill="none" stroke="#2E8B9A" stroke-width="9" d="M 20 84 L 82 84" />' +
          '</g></svg>',
      };

  private SPLIT: any = [0.4, 0.27, 0.15, 0.18];

  private PART_LABELS: any = ['Part A', 'Part B', 'Part C', 'Part D'];

  private PART_COLORS: any = ['#5C3A21', '#C77B30', '#6F9B4A', '#2E8B9A'];

  private seriesFor = (total: any): any => {
        var out = this.SPLIT.map(function (f) {
          return Math.round(total * f)
        })
        // Put the rounding error on the biggest part, so the total is exactly asked.
        out[0] +=
          total -
          out.reduce(function (s, v) {
            return s + v
          }, 0)
        return out
      };

  private inspect = (shape: any, count: any): any => {
        var objects = []
        for (var i = 0; i < count; i++) {
          objects.push({
            id: 'u' + i,
            index: i,
            seriesIndex: 0,
            dataPointIndex: i,
            label: '',
            datum: null,
            r: 2,
          })
        }
        var pos = shape(objects, { x: 0, y: 0, width: 620, height: 400 })
        if (!pos.length) return { placed: 0, rows: 0, thin: 0 }
        var asc = function (a, b) {
          return a - b
        }
  
        // Dots land on shared row baselines, so grouping by y recovers the rows the
        // packer cut the outline into.
        var rows = {}
        pos.forEach(function (p) {
          var k = Math.round(p.y * 2) / 2
          if (!rows[k]) rows[k] = []
          rows[k].push(p.x)
        })
        var keys = Object.keys(rows)
        keys.forEach(function (k) {
          rows[k].sort(asc)
        })
  
        // Counting dots per ROW is not the measure wanted, and a ring is why: a row
        // across the middle of one holds a handful on the left and a handful on the
        // right, and their sum says the mark is comfortably thick when in fact it is
        // two hairlines. So find the typical dot pitch first, then break each row
        // where the gap exceeds it, and measure the CONTIGUOUS runs.
        var gaps = []
        keys.forEach(function (k) {
          var xs = rows[k]
          for (var i = 1; i < xs.length; i++) gaps.push(xs[i] - xs[i - 1])
        })
        gaps.sort(asc)
        var pitch = gaps.length ? gaps[Math.floor(gaps.length * 0.5)] : 0
  
        var runs = []
        keys.forEach(function (k) {
          var xs = rows[k]
          var run = 1
          for (var i = 1; i < xs.length; i++) {
            if (pitch > 0 && xs[i] - xs[i - 1] > pitch * 1.8) {
              runs.push(run)
              run = 1
            } else {
              run++
            }
          }
          runs.push(run)
        })
        runs.sort(asc)
  
        return {
          placed: pos.length,
          rows: keys.length,
          // 10th percentile, not the minimum: the top and bottom row of any rounded
          // mark legitimately holds one dot, so the minimum is always 1 and says
          // nothing.
          thin: runs[Math.floor(runs.length * 0.1)] || 0,
        }
      };

  private state: any = {
          markup: this.PRESETS.Cup,
          source: 'Cup (preset)',
          count: 1200,
          rule: 'nonzero',
          order: 'rows',
          hollow: false,
          adoptRule: true,
        };

  private el: any = {
          drop: document.getElementById('fg-drop'),
          file: document.getElementById('fg-file'),
          paste: document.getElementById('fg-paste'),
          count: document.getElementById('fg-count'),
          countVal: document.getElementById('fg-count-val'),
          order: document.getElementById('fg-order'),
          rule: document.getElementById('fg-rule'),
          hollow: document.getElementById('fg-hollow'),
          src: document.getElementById('fg-src'),
          els: document.getElementById('fg-els'),
          placed: document.getElementById('fg-placed'),
          rows: document.getElementById('fg-rows'),
          thin: document.getElementById('fg-thin'),
          warn: document.getElementById('fg-warn'),
          code: document.getElementById('fg-code'),
          copy: document.getElementById('fg-copy'),
        };

  private commas = (n: any): any => {
          return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
        };

  private esc = (s: any): any => {
          return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
        };

  private lastSnippet: any = '';

  private apply = (): any => {
          var out = this.svgOutline(this.state.markup)
          if (out.error) {
            this.el.warn.className = 'fg-warn is-bad'
            this.el.warn.textContent = out.error
            return
          }
  
          // A newly loaded mark adopts the fill rule its own artwork declares; an
          // explicit click on the toggle is never overridden.
          if (this.state.adoptRule) {
            this.state.adoptRule = false
            if (out.rule && !out.stroked) {
              this.state.rule = out.rule
              this.el.rule.querySelectorAll('button').forEach(function (x) {
                x.classList.toggle(
                  'is-active',
                  x.getAttribute('data-rule') === this.state.rule,
                )
              })
            }
          }
  
          var opts = { name: 'mine' }
          var shape
          if (out.stroked) {
            // No fill anywhere in the file: the strokes ARE the mark.
            opts.width = out.width
            opts.order = this.state.order
            shape = ApexUnitShapes.strokeFrom(out.path, opts)
          } else {
            opts.fillRule = this.state.rule
            opts.order = this.state.order
            shape = ApexUnitShapes.shapeFrom(out.path, opts)
            if (this.state.hollow) shape = ApexUnitShapes.outlined(shape, 7)
          }
  
          // A stroke-only mark has no interior, so fillRule and outlined() have
          // nothing to act on. Dim them rather than leave them live and inert.
          this.el.rule.classList.toggle('is-off', out.stroked)
          this.el.hollow.classList.toggle('is-off', out.stroked)
  
          var stats = this.inspect(shape, this.state.count)
  
          var chart = ApexCharts.getChartByID('logoForge')
          chart.updateOptions(
            {
              series: this.seriesFor(this.state.count),
              plotOptions: { unit: { layout: 'custom', positions: shape } },
            },
            false,
            true,
          )
  
          this.el.src.textContent = this.state.source
          this.el.els.textContent =
            this.commas(out.subpaths) +
            ' from ' +
            this.commas(out.elements) +
            (out.stroked ? ' stroke el.' : ' el.')
          this.el.placed.textContent = this.commas(stats.placed)
          this.el.rows.textContent = this.commas(stats.rows)
          this.el.thin.textContent = this.commas(stats.thin)
  
          // The readability warning, from the measurement rather than from taste.
          if (stats.placed < this.state.count) {
            this.el.warn.className = 'fg-warn is-bad'
            this.el.warn.textContent =
              'Only ' +
              this.commas(stats.placed) +
              ' of ' +
              this.commas(this.state.count) +
              ' dots found a slot. The outline is probably not closed, or it is so thin' +
              ' at this dot count that whole rows hold nothing.'
          } else if (stats.thin <= 1) {
            this.el.warn.className = 'fg-warn'
            this.el.warn.textContent =
              'Parts of this mark are down to a single file of dots. Raise the dot' +
              ' count, or set minUnits to about ' +
              this.commas(Math.round(this.state.count * 2.2)) +
              ' so the chart warns instead of drawing mush.'
          } else if (stats.thin <= 2) {
            this.el.warn.className = 'fg-warn'
            this.el.warn.textContent =
              'Thin, but still reading: two dots across at the narrowest. This is about' +
              ' the floor for this mark, so it is a fair value for minUnits.'
          } else {
            this.el.warn.className = 'fg-warn fg-hide'
          }
  
          this.emit(out, stats)
        };

  private emit = (out: any, stats: any): any => {
          var d = out.path
          var shown = d.length > 200 ? d.slice(0, 200) + ' ...' : d
          var fn = out.stroked ? 'strokeFrom' : 'shapeFrom'
          var opts = out.stroked
            ? '  width: ' + this.round2(out.width) + ',\n'
            : "  fillRule: '" + this.state.rule + "',\n"
          var minU = Math.max(
            100,
            Math.round(this.state.count * (stats.thin <= 2 ? 1 : 0.5)),
          )
  
          this.lastSnippet =
            "import ApexCharts from 'apexcharts'\n" +
            'import { ' +
            fn +
            " } from 'apexcharts/unit-shapes'\n\n" +
            '// ' +
            out.subpaths +
            ' subpath(s) sampled from ' +
            this.state.source +
            '\n' +
            'const MY_LOGO = ' +
            JSON.stringify(shown) +
            '\n\n' +
            'const mine = ' +
            fn +
            '(MY_LOGO, {\n' +
            opts +
            "  order: '" +
            this.state.order +
            "',\n" +
            '  minUnits: ' +
            minU +
            ',\n' +
            '})\n\n' +
            'new ApexCharts(host, {\n' +
            "  chart: { type: 'unit' },\n" +
            '  series: [' +
            this.seriesFor(this.state.count).join(', ') +
            '],\n' +
            '  labels: ' +
            JSON.stringify(this.PART_LABELS) +
            ',\n' +
            "  plotOptions: { unit: { layout: 'custom', positions: mine } },\n" +
            '}).render()'
  
          this.el.code.innerHTML = this.esc(this.lastSnippet)
            .replace(/^(\/\/.*)$/gm, '<span class="c">$1</span>')
            .replace(/(&quot;|')((?:[^'&]|&(?!quot;))*)\1/g, function (m) {
              return '<span class="s">' + m + '</span>'
            })
        };

  private load = (markup: any, label: any): any => {
          this.state.markup = markup
          this.state.source = label
          this.state.adoptRule = true
          document.querySelectorAll('[data-preset]').forEach(function (b) {
            b.classList.toggle('is-active', b.getAttribute('data-preset') === label)
          })
          this.apply()
        };

  private readFile = (file: any): any => {
          if (!file) return
          var reader = new FileReader()
          reader.onload = function () {
            this.load(String(reader.result), file.name)
          }
          reader.readAsText(file)
        };

  public chartOptions: Partial<ChartOptions> = {
          series: [480, 324, 180, 216],
          chart: {
            id: 'logoForge',
            type: 'unit',
            height: 400,
            fontFamily: 'Helvetica, Arial, sans-serif',
            animations: {
              enabled: true,
              speed: 700,
            },
            toolbar: { show: false },
          },
          labels: ['Part A', 'Part B', 'Part C', 'Part D'],
          colors: ['#5C3A21', '#C77B30', '#6F9B4A', '#2E8B9A'],
          legend: {
            position: 'bottom',
          },
          plotOptions: {
            unit: {
              layout: 'custom',
              // Replaced on every change by the forge below; the initial value is the
              // Halo preset, built in the same way a dropped file is.
              positions: undefined,
              // 'identity' would need per-datum ids; 'flow' keys the dots by global order,
              // so the same crowd migrates from one mark into the next instead of being
              // rebuilt, which is what makes swapping logos feel like one continuous
              // object rather than a slideshow.
              transition: 'flow',
              size: 2.2,
              clusterLabels: {
                show: false,
              },
            },
          },
        };
  ngAfterViewInit() {
    (window as any).ApexCharts.setLicense('APEX-eyJleHBpcnlEYXRlIjoiMjEyNi0wNy0wNCIsImlzc3VlRGF0ZSI6IjIwMjYtMDctMjgiLCJwbGFuIjoicHJlbWl1bSIsImRvbWFpbnMiOlsiYXBleGNoYXJ0cy5jb20iLCIxMjcuMC4wLjEiLCJsb2NhbGhvc3QiXSwic2lnIjoieVBmb1VCc0Z3TU9ZdUEyaEZkR0I2Y1FtZ0JITUtXcVdJSjB2NVRESXRZbFR3eDJMUmh6R2x0RUc3VXJ4X0s3b25ZMWRZb2Z2VGItN01ydFYyNDVyOWcifQ==');
    this.apply()

    function apply() {
            var out = this.svgOutline(this.state.markup)
            if (out.error) {
              this.el.warn.className = 'fg-warn is-bad'
              this.el.warn.textContent = out.error
              return
            }

            // A newly loaded mark adopts the fill rule its own artwork declares; an
            // explicit click on the toggle is never overridden.
            if (this.state.adoptRule) {
              this.state.adoptRule = false
              if (out.rule && !out.stroked) {
                this.state.rule = out.rule
                this.el.rule.querySelectorAll('button').forEach(function (x) {
                  x.classList.toggle(
                    'is-active',
                    x.getAttribute('data-rule') === this.state.rule,
                  )
                })
              }
            }

            var opts = { name: 'mine' }
            var shape
            if (out.stroked) {
              // No fill anywhere in the file: the strokes ARE the mark.
              opts.width = out.width
              opts.order = this.state.order
              shape = ApexUnitShapes.strokeFrom(out.path, opts)
            } else {
              opts.fillRule = this.state.rule
              opts.order = this.state.order
              shape = ApexUnitShapes.shapeFrom(out.path, opts)
              if (this.state.hollow) shape = ApexUnitShapes.outlined(shape, 7)
            }

            // A stroke-only mark has no interior, so fillRule and outlined() have
            // nothing to act on. Dim them rather than leave them live and inert.
            this.el.rule.classList.toggle('is-off', out.stroked)
            this.el.hollow.classList.toggle('is-off', out.stroked)

            var stats = this.inspect(shape, this.state.count)

            var chart = ApexCharts.getChartByID('logoForge')
            this.chart.updateOptions(
              {
                series: this.seriesFor(this.state.count),
                plotOptions: { unit: { layout: 'custom', positions: shape } },
              },
              false,
              true,
            )

            this.el.src.textContent = this.state.source
            this.el.els.textContent =
              this.commas(out.subpaths) +
              ' from ' +
              this.commas(out.elements) +
              (out.stroked ? ' stroke el.' : ' el.')
            this.el.placed.textContent = this.commas(stats.placed)
            this.el.rows.textContent = this.commas(stats.rows)
            this.el.thin.textContent = this.commas(stats.thin)

            // The readability warning, from the measurement rather than from taste.
            if (stats.placed < this.state.count) {
              this.el.warn.className = 'fg-warn is-bad'
              this.el.warn.textContent =
                'Only ' +
                this.commas(stats.placed) +
                ' of ' +
                this.commas(this.state.count) +
                ' dots found a slot. The outline is probably not closed, or it is so thin' +
                ' at this dot count that whole rows hold nothing.'
            } else if (stats.thin <= 1) {
              this.el.warn.className = 'fg-warn'
              this.el.warn.textContent =
                'Parts of this mark are down to a single file of dots. Raise the dot' +
                ' count, or set minUnits to about ' +
                this.commas(Math.round(this.state.count * 2.2)) +
                ' so the chart warns instead of drawing mush.'
            } else if (stats.thin <= 2) {
              this.el.warn.className = 'fg-warn'
              this.el.warn.textContent =
                'Thin, but still reading: two dots across at the narrowest. This is about' +
                ' the floor for this mark, so it is a fair value for minUnits.'
            } else {
              this.el.warn.className = 'fg-warn fg-hide'
            }

            this.emit(out, stats)
          }
    ['dragenter', 'dragover'].forEach(function (ev) {
            this.el.drop.addEventListener(ev, (e) => {
              e.preventDefault()
              this.el.drop.classList.add('is-over')
            })
          })
          ;
    ['dragleave', 'drop'].forEach(function (ev) {
            this.el.drop.addEventListener(ev, (e) => {
              e.preventDefault()
              this.el.drop.classList.remove('is-over')
            })
          })
    this.el.drop.addEventListener('drop', (e) => {
            var dt = e.dataTransfer
            if (dt && dt.files && dt.files.length) this.readFile(dt.files[0])
          })
    this.el.drop.addEventListener('click', (e) => {
            if (e.target !== this.el.paste) this.el.file.click()
          })
    this.el.file.addEventListener('change', () => {
            this.readFile(this.el.file.files[0])
          })
    this.el.paste.addEventListener('input', () => {
            var v = this.el.paste.value.trim()
            if (v.indexOf('<svg') !== -1) this.load(v, 'pasted markup')
          })
    this.el.count.addEventListener('input', () => {
            this.state.count = Number(this.el.count.value)
            this.el.countVal.textContent = this.commas(this.state.count)
            this.apply()
          })
    this.el.order.addEventListener('change', () => {
            this.state.order = this.el.order.value
            this.apply()
          })
    this.el.rule.addEventListener('click', (e) => {
            var b = e.target.closest('[data-rule]')
            if (!b) return
            this.state.rule = b.getAttribute('data-rule')
            this.el.rule.querySelectorAll('button').forEach(function (x) {
              x.classList.toggle('is-active', x === b)
            })
            this.apply()
          })
    this.el.hollow.addEventListener('click', (e) => {
            var b = e.target.closest('[data-hollow]')
            if (!b) return
            this.state.hollow = b.getAttribute('data-hollow') === '1'
            this.el.hollow.querySelectorAll('button').forEach(function (x) {
              x.classList.toggle('is-active', x === b)
            })
            this.apply()
          })
    document.querySelectorAll('[data-preset]').forEach(function (b) {
            b.addEventListener('click', () => {
              var name = b.getAttribute('data-preset')
              this.load(this.PRESETS[name], name)
            })
          })
    this.el.copy.addEventListener('click', () => {
            if (navigator.clipboard) navigator.clipboard.writeText(this.lastSnippet)
            this.el.copy.textContent = 'Copied'
            window.setTimeout(function () {
              this.el.copy.textContent = 'Copy'
            }, 1400)
          }
  }

  ngOnDestroy() {
    // no cleanup needed
  }
}