
/*___________________________________________________________________________*/
class LimWellplateTableBase extends LimTableDataClientExtender(HTMLElement) {
   #header
   #body
   #footer
   #table
   #legend
   #containerSizeObserver
   #tableRowVisibility
   #plateColCount
   #plateColNaming
   #plateRowCount
   #plateRowNaming
   #wellColId
   #wellSize

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.className = "lim-plate-view";

      this.#table = document.createElement("table");
      this.#table.className = "lim-wellplate-table";

      this.title = pars?.title ?? "";
      this.dataSelectionEnabled = pars?.dataSelectionEnabled ?? true;
      this.legendVisibility = pars?.legendVisibility ?? "below";

      this.#legend = null;
      this.#plateColCount = null;
      this.#plateColNaming = null;
      this.#plateRowCount = null;
      this.#plateRowNaming = null;
      this.#wellColId = null;
      this.#wellSize = -1;
      this.#tableRowVisibility = pars?.tableRowVisibility ?? "all";

      this.optionList = new Map();

      if (0 < (pars?.tableRowSelectVisibility?.length ?? 0)) {
         this.optionList.set("selectedRowVisibility", {
            type: "selection",
            title: "Show data for"/*,
            iconres: "w32_baseresources/res/AutoSize_columns.svg"
            */
         });
      }

      this.#containerSizeObserver = new ResizeObserver((entries) => {
         for (const entry of entries) {
            this.calculateWellSize(entry.contentRect.width, entry.contentRect.height);
         }
         this.recreate();
      });
   }

   get wellSize() {
      if (this.#wellSize < 0)
         this.calculateWellSize();
      return this.#wellSize;
   }

   calculateWellSize(containerW, containerH) {
      let width = 0;
      let height = 0;
      if ((typeof containerW === "undefined" || typeof containerH === "undefined") && document.body.contains(this)) {
         const style = window.getComputedStyle(this);
         width = parseInt(style.width, 10);
         height = parseInt(style.height, 10);
      }
      else if (typeof containerW === "number" && typeof containerH === "number") {
         width = containerW;
         height = containerH;
      }
      else {
         this.#wellSize = -1;
         return;
      }

      if (!this.#plateColCount || !this.#plateRowCount) {
         this.#wellSize = -1;
         return;
      }

      if (this.#footer)
         height -= 50;

      if (this.#header)
         height -= 50;

      const wellw = width / (this.#plateColCount + 0.5);
      const wellh = height / (this.#plateRowCount + 0.5);
      this.#wellSize = Math.min(Math.floor(wellw), Math.floor(wellh));
   }

   get cellFontSize() {
      return this.wellSize / 5;
   }

   get headerFontSize() {
      return this.wellSize / 4;
   }

   get wellContentSize() {
      return this.wellSize - 1;
   }

   connectedCallback() {
      if (this.title?.length) {
         this.#header = document.createElement("div");
         this.#header = document.createElement('div');
         this.#header.className = 'lim-header';
         this.#header.innerHTML = `<h1>${pars.title}</h1>`
         this.appendChild(this.#header);
      }

      this.#body = document.createElement('div');
      this.#body.className = 'lim-body';
      this.#body.appendChild(this.#table);
      this.appendChild(this.#body);

      if (this.legendVisibility === "below") {
         this.#legend = document.createElement('div');
         this.#legend.className = 'lim-legend';
         this.#footer = document.createElement('div');
         this.#footer.className = 'lim-footer';
         this.#footer.appendChild(this.#legend);
         this.appendChild(this.#footer);
      }

      this.connectToDocument();
      this.fetchAndUpdateTable();
      this.#containerSizeObserver.observe(this);
   }

   disconnectedCallback() {
      this.#containerSizeObserver.disconnect();

      if (this.#header)
         this.removeChild(this.#header);
      this.removeChild(this.#body);
      if (this.#footer)
         this.removeChild(this.#footer);
      this.disconnectFromDocument();
   }

   get tableRowVisibility() {
      return this.#tableRowVisibility
   }

   set tableRowVisibility(val) {
      if (this.#tableRowVisibility !== val)
         this.#tableRowVisibility = val;
   }

   get table() {
      return this.#table;
   }

   get legend() {
      return this.#legend;
   }

   get wellColId() {
      return this.#wellColId;
   }

   set errorMessage(text) {
      if (this.legend) {
         while (this.legend.children.length)
            this.legend.removeChild(this.legend.children[0]);
      }
      if (typeof text === "string") {
         if (this.legend) {
            let label = document.createElement('span');
            label.className = 'lim-error';
            label.innerText = text;
            this.legend.appendChild(label);
         }
         console.log(text);
      }
   }

   onTableChanged() {
      this.fetchAndUpdateTable();
   }

   fetchAndUpdateTable() {
      this.fetchTableMetadata(() => {
         this.recreate();
      });
   }

   recreate() {
      this.#wellColId = null;
      this.errorMessage = null;
      this.#plateColCount = null;
      this.#plateColNaming = null;
      this.#plateRowCount = null;
      this.#plateRowNaming = null;
      if (!document.body.contains(this))
         return false;
      if (!this.tableData)
         return false;
      for (let i = 0; i < this.tableData.colCount; i++) {
         const meta = this.tableData.colMetadataAt(i);
         if (this.tableData.colIdAt(i) === "_Well" && meta?.wellPlateInfo) {
            this.#plateColCount = meta.wellPlateInfo.columnCount;
            this.#plateColNaming = meta.wellPlateInfo.columnNaming;
            this.#plateRowCount = meta.wellPlateInfo.rowCount;
            this.#plateRowNaming = meta.wellPlateInfo.rowNaming;
            this.#wellColId = this.tableData.colIdAt(i);
            break;
         }
      }

      if (!this.#wellColId) {
         this.errorMessage = "Not a well table: Well column missing!";
         return false;
      }


      let pars = {};
      const colIdList = [...new Set(this.tableData.systemColIdList.concat(this?.colIdList ?? [])).values()];
      if (0 < colIdList.length && colIdList.length < this.tableData.colCount)
         pars.cols = JSON.stringify(colIdList);
      const rowFilterList = this.makeDefaultRowFilterList();
      if (0 < rowFilterList.length)
         pars.filter = JSON.stringify({ op: "all", filters: rowFilterList });
      this.fetchTableData(pars, () => {
         this.updateData();
      });
   }

   wellToRowsMap() {
      let wells = {};
      const well = this.tableData.colData(this.wellColId);
      for (let j = 0; j < this.tableData.rowCount; j++)
         wells[well[j]] = j;
      return wells;
   }

   rootStyles() {
      const r = document.querySelector(":root");
      var rs = getComputedStyle(r);
      return {
         color_gray_0: rs.getPropertyValue("--color-gray-0"),
         color_gray_1: rs.getPropertyValue("--color-gray-1"),
         color_gray_2: rs.getPropertyValue("--color-gray-2"),
         color_gray_3: rs.getPropertyValue("--color-gray-3"),
         color_gray_4: rs.getPropertyValue("--color-gray-4"),
         color_gray_5: rs.getPropertyValue("--color-gray-5"),
         color_gray_6: rs.getPropertyValue("--color-gray-6"),
         color_gray_7: rs.getPropertyValue("--color-gray-7"),
         color_gray_8: rs.getPropertyValue("--color-gray-8"),
         darken_by: parseFloat(rs.getPropertyValue("--darken-by"))
      };
   };

   createHeaderCell(name, width, height, fontSize) {
      let cell = document.createElement("th");
      cell.innerText = name;
      cell.style.width = width;
      cell.style.height = height;
      cell.style.fontSize = fontSize;
      return cell;
   }

   wellNameShort(row, col) {
      let ret = "";
      const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
      if (this.#plateRowNaming == "letter") {
         ret += letters[row];
      }
      else {
         ret += String(row+1);
      }
      if (this.#plateColNaming == "letter") {
         ret += letters[col];
      }
      else {
         ret += String(col+1);
      }
      return ret;
   }

   wellNameWithLeadingZeros(row, col) {
      function pad(num, size) {
         num = num.toString();
         while (num.length < size) num = "0" + num;
         return num;
      }
      let ret = "";
      const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
      if (this.#plateRowNaming == "letter") {
         ret += letters[row];
      }
      else {
         ret += pad(row+1, Math.ceil(Math.log10(this.#plateRowCount)));
      }
      if (this.#plateColNaming == "letter") {
         ret += letters[col];
      }
      else {
         ret += pad(col+1, Math.ceil(Math.log10(this.#plateColCount)));
      }
      return ret;
   }

   populateTable(fillDataCellFn) {
      if (!this.tableData || !this.#wellColId)
         return;

      try {
         const cellSize = `${this.wellSize}px`;
         const cellHalfSize = `${Math.floor(this.wellSize / 2)}px`;
         const cellFontSize = `${this.cellFontSize}px`;
         const headerFontSize = `${this.headerFontSize}px`;

         const wellToRows = this.wellToRowsMap();
         const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
         const plateColNames = [...limRange(this.#plateColCount)].map(item => (this.#plateColNaming === "letter" ? letters[item] : String(item + 1)));
         const plateRowNames = [...limRange(this.#plateRowCount)].map(item => (this.#plateRowNaming === "letter" ? letters[item] : String(item + 1)));

         this.table.deleteTHead();
         for (let e of this.getElementsByTagName("tbody"))
            this.table.removeChild(e);

         const thead = this.table.createTHead();
         let row = thead.insertRow(-1);
         row.appendChild(this.createHeaderCell("", cellHalfSize, cellHalfSize, headerFontSize));
         for (let i = 0; i < this.#plateColCount; i++) {
            row.appendChild(this.createHeaderCell(plateColNames[i], cellSize, cellHalfSize, headerFontSize));
         }

         let selectedWellName = "";
         const selRow = this.tableData.loopIndexesToRowIndex(this.currentLoopIndexes);
         if (0 <= selRow && this.#wellColId) {
            const well = this.tableData.colData(this.#wellColId);
            selectedWellName = well[selRow];
         }

         const tbody = document.createElement("tbody");
         for (let j = 0; j < this.#plateRowCount; j++) {
            row = tbody.insertRow();
            row.appendChild(this.createHeaderCell(plateRowNames[j], cellHalfSize, cellSize, headerFontSize));
            for (let i = 0; i < this.#plateColCount; i++) {
               let wellName = this.wellNameWithLeadingZeros(j, i);
               if (!wellToRows.hasOwnProperty(wellName))
                  wellName = this.wellNameShort(j, i);
               const cell = row.insertCell(-1);
               cell.style.width = cellSize;
               cell.style.height = cellSize;
               cell.style.fontSize = cellFontSize;
               cell.onclick = e => this.wellClicked(e, wellName);
               fillDataCellFn(cell, wellName);
               if (this.dataSelectionEnabled && wellName === selectedWellName) {
                  cell.classList.add("selected");
               }
            }
         }
         this.table.appendChild(tbody);
      }
      catch (e) {
         console.error(e);
      }
   }

   onCurrentLoopIndexesChanged(val, change) {
      if (!this.tableData || !this.#wellColId)
         return;

      // if anything else than w changes reload
      if (this.isSelectedRowVisiblityIncludingAnyOfLoops(change))
         this.fetchAndUpdateTable();

      this.updateWellSelection(this.tableData.loopIndexesToRowIndex(val));
   }

   onSelectedRowVisibilityChanged(val, oldVal) {
      this.fetchAndUpdateTable();
   }

   updateWellSelection(selRow) {
      if (!this.tableData || !this.#wellColId || !this.dataSelectionEnabled)
         return;
      try {
         let selectedWellName = "";
         if (0 <= selRow) {
            const well = this.tableData.colData(this.#wellColId);
            selectedWellName = well[selRow];
         }

         const wellToRows = this.wellToRowsMap();
         let trs = this.getElementsByTagName("tbody").item(0).getElementsByTagName("tr");
         for (let j = 0; j < this.#plateRowCount; j++) {
            let row = trs.item(j);
            for (let i = 0; i < this.#plateColCount; i++) {
               let cell = row.children.item(i + 1); // header
               let wellName = this.wellNameWithLeadingZeros(j, i);
               if (!wellToRows.hasOwnProperty(wellName))
                  wellName = this.wellNameShort(j, i);
               if (wellName === selectedWellName) {
                  cell.classList.add("selected");
               }
               else
                  cell.classList.remove("selected");
            }
         }
      }
      catch (e) {
         console.error(e);
      }
   }

   wellClicked(event, wellName) {
      if (!this.tableData || !this.#wellColId || !this.dataSelectionEnabled)
         return;

      const well = this.tableData.colData(this.#wellColId);
      const wellIndex = this.tableData.colData("_loopWellIndex");
      const index = well.indexOf(wellName);
      if (index !== -1 && wellIndex[index] !== null) {
         this.updateWellSelection(index);
         const loopIndexes = this.tableData.rowIndexToLoopIndexes(index);
         setTimeout(() => { this.currentLoopIndexes = loopIndexes });
      }
   }

   makeFeatureMap(allowedFeatures, forbiddenFeatures, predicate) {
      if (!this.tableData)
         return;
      const td = this.tableData;
      const allowed = allowedFeatures ? td.matchColsFulltext(allowedFeatures) : [...limRange(td.colCount)];
      const forbidden = forbiddenFeatures ? td.matchColsFulltext(forbiddenFeatures) : [];

      const map = new Map();
      for (let i = 0; i < td.colCount; i++) {
         const meta = td.colMetadataAt(i);
         if ((meta?.hidden ?? false) === false && allowed.includes(i) && !forbidden.includes(i) && (predicate === undefined || predicate(meta))) {
            map.set(td.colIdAt(i), td.colTitleAt(i));
         }
      }
      return map;
   }

   makeNumericFeatureMap(allowedFeatures, forbiddenFeatures) {
      return this.makeFeatureMap(allowedFeatures, forbiddenFeatures, meta => (meta.decltype === "int" || meta.decltype === "double"));
   }

   createOptionProperties(name, title, opt = {}) {
      if (!opt?.hidden)
         this.optionList.set(name, { type: "option", title: title, iconres: opt?.iconres });
      Object.defineProperties(this, {
         [`${name}OptionValue`] : {
            get() { return this[name]; },
            set(val) {
               if (this[name] === val)
                  return;
               this[name] = val;
               this.recreate();
            }
         },
         [`${name}OptionValueChanged`]: { value: new LimSignal([]), writable: false }
      });

      if (typeof opt?.enabled === "string") {
         Object.defineProperties(this, {
            [`${name}OptionEnabled`]: { get() { return this[opt.enabled]; } },
            [`${name}OptionEnabledChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      else if (typeof opt?.enabled === "function") {
         Object.defineProperties(this, {
            [`${name}OptionEnabled`]: { get() { return opt.enabled(); } },
            [`${name}OptionEnabledChanged`]: { value: new LimSignal([]), writable: false }
         });
      }
   }

   createSelectionProperties(name, title, values, opt = {}) {
      if (!opt?.hidden)
         this.optionList.set(name, { type: "selection", title: title });
      Object.defineProperties(this, {
         [`${name}OptionValue`]: {
            get() { return this[name]; },
            set(val) {
               if (JSON.stringify(this[name]) === JSON.stringify(val))
                  return;
               this[name] = val;
               this.recreate();
            }
         },
         [`${name}OptionValueChanged`]: { value: new LimSignal([]), writable: false },
      });

      if (typeof values === "string") {
         Object.defineProperties(this, {
            [`${name}OptionValues`]: { get() { return this[values]; } },
            [`${name}OptionValuesChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      else if (typeof values === "function") {
         Object.defineProperties(this, {
            [`${name}OptionValues`]: { get() { return values(); } },
            [`${name}OptionValuesChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      if (typeof opt?.enabled === "string") {
         Object.defineProperties(this, {
            [`${name}OptionEnabled`]: { get() { return this[opt.enabled]; } },
            [`${name}OptionEnabledChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      else if (typeof opt?.enabled === "function") {
         Object.defineProperties(this, {
            [`${name}OptionEnabled`]: { get() { return opt.enabled(); } },
            [`${name}OptionEnabledChanged`]: { value: new LimSignal([]), writable: false }
         });
      }
   }

   createMultiSelectionProperties(name, title, values, opt = {}) {
      if (!opt?.hidden)
         this.optionList.set(name, { type: "multi-selection", title: title });
      Object.defineProperties(this, {
         [`${name}OptionValue`] : {
            get() { return this[name]; },
            set(val) {
               if (JSON.stringify(this[name]) === JSON.stringify(val))
                  return;
               this[name] = val;
               this.recreate();
            }
         },
         [`${name}OptionValueChanged`]: { value: new LimSignal([]), writable: false },
      });

      if (typeof values === "string") {
         Object.defineProperties(this, {
            [`${name}OptionValues`]: { get() { return this[values]; } },
            [`${name}OptionValuesChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      else if (typeof values === "function") {
         Object.defineProperties(this, {
            [`${name}OptionValues`]: { get() { return values(); } },
            [`${name}OptionValuesChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      if (typeof opt?.enabled === "string") {
         Object.defineProperties(this, {
            [`${name}OptionEnabled`]: { get() { return this[opt.enabled]; } },
            [`${name}OptionEnabledChanged`]: { value: new LimSignal([]), writable: false }
         });
      }

      else if (typeof opt?.enabled === "function") {
         Object.defineProperties(this, {
            [`${name}OptionEnabled`]: { get() { return opt.enabled(); } },
            [`${name}OptionEnabledChanged`]: { value: new LimSignal([]), writable: false }
         });
      }
   }
}

/*___________________________________________________________________________*/
class LimPlateViewBars extends LimWellplateTableBase {
   #dataColIds
   #dataDefaultFeature
   #dataAllowedFeatures
   #dataForbiddenFeatures
   #dataFeatureMap
   #dataMinimum
   #dataMaximum
   #dataIsLog
   #barWidth

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add("lim-plate-view-bars");
      this.table.classList.add("lim-wellplate-table-bars");

      this.name = pars?.name || "Bars";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/barchart_double.svg";

      this.#dataColIds = pars?.dataColIds ?? null;
      this.#dataDefaultFeature = pars?.dataDefaultFeature ? pars.dataDefaultFeature : null;
      this.#dataAllowedFeatures = pars?.dataAllowedFeatures ? pars.dataAllowedFeatures : null;
      this.#dataForbiddenFeatures = pars?.dataForbiddenFeatures ? pars.dataForbiddenFeatures : null;
      this.#dataIsLog = !!pars?.dataIsLog;
      this.#dataFeatureMap = new Map();
      this.#dataMinimum = LimGraphBokeh.tryNumber(pars?.dataMinimum);
      this.#dataMaximum = LimGraphBokeh.tryNumber(pars?.dataMaximum);
      this.#barWidth = LimGraphBokeh.tryNumber(pars?.barWidth) ?? 0.8;

      if (pars?.dataOptionVisible ?? true) {
         this.optionList.set("data", {
            type: "multi-selection",
            title: "Data"
         });
      }

      Object.defineProperties(this, {
         dataOptionValues: {
            get() { return [...this.#dataFeatureMap.entries()]; }
         },
         dataOptionValuesChanged: {
            value: new LimSignal(pars?.dataOptionValuesChanged ? [pars.dataOptionValuesChanged] : []),
            writable: false
         },
         dataOptionValue: {
            get() { return this.dataColIds; },
            set(val) { this.dataColIds = val; }
         },
         dataOptionValueChanged: {
            value: new LimSignal(pars?.dataOptionValueChanged ? [pars.dataOptionValueChanged] : []),
            writable: false
         }
      });
   }

   get colIdList() {
      const td = this.tableData;

      const dataFeatureMap = this.makeNumericFeatureMap(this.#dataAllowedFeatures, this.#dataForbiddenFeatures);
      if (JSON.stringify([...this.#dataFeatureMap]) !== JSON.stringify([...dataFeatureMap])) {
         this.#dataFeatureMap = dataFeatureMap;
         this.dataOptionValuesChanged.emit(this);
      }

      let dataColIds = Array.isArray(this.#dataColIds) ? this.#dataColIds.filter(item => [...this.#dataFeatureMap.keys()].includes(item)) : null;
      if (null === dataColIds && Array.isArray(this.#dataDefaultFeature))
         dataColIds = this.#dataDefaultFeature.map(item => td.colIdAt(td.matchColsFulltext(item)[0] ?? -1)).filter(item => item);
      else if (null === dataColIds && typeof this.#dataDefaultFeature === "string")
         dataColIds = [td.colIdAt(td.matchColsFulltext(this.#dataDefaultFeature)[0] ?? -1)];

      if (JSON.stringify(this.#dataColIds) !== JSON.stringify(dataColIds)) {
         this.#dataColIds = dataColIds;
         this.dataOptionValueChanged.emit(this);
      }

      return this.#dataColIds;
   }

   updateData() {
      const td = this.tableData;
      let wells = this.wellToRowsMap();
      if (this?.dataColIds?.length) {
         const colData = this.dataColIds.map(item => td.colData(item));
         const colMeta = this.dataColIds.map(item => td.colMetadata(item));
         const colDataFiltered = this.#dataIsLog
            ? colData.map(data => data.filter(item => item !== null && Number.isFinite(item) && 0 < item))
            : colData.map(data => data.filter(item => item !== null && Number.isFinite(item)));

         let min = 0, max = 0;
         if (this.#dataIsLog) {
            min = (this.#dataMinimum != null ? Math.log10(this.#dataMinimum) : null) ?? Math.min(...colDataFiltered.map((data, index) => colMeta?.[index]?.globalRange?.logMin ?? Math.log10(Math.min(...data))));
            max = (this.#dataMaximum != null ? Math.log10(this.#dataMaximum) : null) ?? Math.max(...colDataFiltered.map((data, index) => colMeta?.[index]?.globalRange?.logMax ?? Math.log10(Math.max(...data))));
         }
         else {
            min = this.#dataMinimum ?? Math.min(...colDataFiltered.map((data, index) => colMeta?.[index]?.globalRange?.min ?? Math.min(...data)));
            max = this.#dataMaximum ?? Math.max(...colDataFiltered.map((data, index) => colMeta?.[index]?.globalRange?.max ?? Math.max(...data)));
         }

         const rng = max - min;
         const colors = Object.values(LimGraphBokeh.tableauColorList());
         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);
            for (let i = 0; i < colMeta.length; i++) {
               let item = document.createElement('span')
               item.className = 'lim-item';
               let glyph = document.createElement('span');
               glyph.className = 'lim-glyph';
               glyph.style.backgroundColor = colors[i % colors.length];
               item.appendChild(glyph);
               let label = document.createElement('span');
               label.className = 'lim-label';
               label.innerText = colMeta[i]?.title;
               item.appendChild(label);
               this.legend.appendChild(item);
            }
         }

         const barWidth = Math.max(0, Math.min(this.#barWidth, 1));
         const size = this.wellContentSize;
         const n = colData.length;
         const margin = Math.round(size * 0.1);
         const content = size - 2 * margin;
         const step = content / n;
         const width = (barWidth * content / n);
         const first = (step - width) / 2;

         this.populateTable((cell, wellName) => {
            let bars = [];
            const row = wells[wellName] ?? -1;
            if (row < 0)
               return;
            for (let i = 0; i < colData.length; i++) {
               const val = Math.max(min, Math.min(max, colData[i][row]));
               if (val === null || isNaN(val) || (this.#dataIsLog && val <= 0))
                  continue;
               const x = margin + first + i * step;
               const w = width;
               const height = Math.max(0, Math.min(size, Math.round(size * ((this.#dataIsLog ? Math.log10((val - min) * 9.0 / rng + 1) : (val - min) / rng)))));
               const y = size - height;
               bars.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${colors[i % colors.length]}" outline="none" />`);
            }
            if (0 === bars.length)
               return;

            cell.innerHTML = `
               <div style="top:${-size/2}px;width:${size};height:${size};position:relative;">
                  <svg viewbox="0 0 ${size} ${size}" style="top:0;left:0;width:${size};height:${size};position:absolute;">${bars.join('\n')}</svg>
               </div>`;
            cell.className = 'selenabled';
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No data: zero columns checked";
      }
      return true;
   }

   get dataColIds() {
      return this.#dataColIds;
   }

   set dataColIds(val) {
      if (this.#dataColIds === val)
         return;
      this.#dataColIds = val;
      this.dataOptionValueChanged.emit(this);
      this.recreate();
   }
}

/*___________________________________________________________________________*/
class LimPlateViewBarStack extends LimWellplateTableBase {
   #dataColIds
   #dataDefaultFeature
   #dataAllowedFeatures
   #dataForbiddenFeatures
   #dataFeatureMap
   #dataMinimum
   #dataMaximum
   #barWidth

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-barstack');
      this.table.classList.add("lim-wellplate-table-barstack");

      this.name = pars?.name || "Barstack";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/barchart_single.svg";

      this.#dataColIds = pars?.dataColIds ?? null;
      this.#dataDefaultFeature = pars?.dataDefaultFeature ? pars.dataDefaultFeature : null;
      this.#dataAllowedFeatures = pars?.dataAllowedFeatures ? pars.dataAllowedFeatures : null;
      this.#dataForbiddenFeatures = pars?.dataForbiddenFeatures ? pars.dataForbiddenFeatures : null;
      this.#dataFeatureMap = new Map();
      this.#dataMinimum = LimGraphBokeh.tryNumber(pars?.dataMinimum);
      this.#dataMaximum = LimGraphBokeh.tryNumber(pars?.dataMaximum);
      this.#barWidth = LimGraphBokeh.tryNumber(pars?.barWidth) ?? 0.8;

      if (pars?.dataOptionVisible ?? true) {
         this.optionList.set("data", {
            type: "multi-selection",
            title: "Data"
         });
      }

      Object.defineProperties(this, {
         dataOptionValues: {
            get() { return [...this.#dataFeatureMap.entries()]; }
         },
         dataOptionValuesChanged: {
            value: new LimSignal(pars?.dataOptionValuesChanged ? [pars.dataOptionValuesChanged] : []),
            writable: false
         },
         dataOptionValue: {
            get() { return this.dataColIds; },
            set(val) { this.dataColIds = val; }
         },
         dataOptionValueChanged: {
            value: new LimSignal(pars?.dataOptionValueChanged ? [pars.dataOptionValueChanged] : []),
            writable: false
         }
      });
   }

   get colIdList() {
      const td = this.tableData;

      const dataFeatureMap = this.makeNumericFeatureMap(this.#dataAllowedFeatures, this.#dataForbiddenFeatures);
      if (JSON.stringify([...this.#dataFeatureMap]) !== JSON.stringify([...dataFeatureMap])) {
         this.#dataFeatureMap = dataFeatureMap;
         this.dataOptionValuesChanged.emit(this);
      }

      let dataColIds = Array.isArray(this.#dataColIds) ? this.#dataColIds.filter(item => [...this.#dataFeatureMap.keys()].includes(item)) : null;
      if (null === dataColIds && Array.isArray(this.#dataDefaultFeature))
         dataColIds = this.#dataDefaultFeature.map(item => td.colIdAt(td.matchColsFulltext(item)[0] ?? -1)).filter(item => item);
      else if (null === dataColIds && typeof this.#dataDefaultFeature === "string")
         dataColIds = [td.colIdAt(td.matchColsFulltext(this.#dataDefaultFeature)[0] ?? -1)];

      if (JSON.stringify(this.#dataColIds) !== JSON.stringify(dataColIds)) {
         this.#dataColIds = dataColIds;
         this.dataOptionValueChanged.emit(this);
      }

      return this.#dataColIds;
   }

   updateData() {
      const td = this.tableData;
      let wells = this.wellToRowsMap();
      if (this?.dataColIds?.length) {
         const colData = this.dataColIds.map(item => td.colData(item));
         const colMeta = this.dataColIds.map(item => td.colMetadata(item));

         let sum = colData[0].map(() => 0);
         colData.forEach(item => {
            item.forEach((val, i) => sum[i] += (val !== null && Number.isFinite(val) && val > 0 ? val : 0));
         });

         const min = this.#dataMinimum ?? 0;
         const max = this.#dataMaximum ?? Math.max(...sum);
         const rng = max - min;

         const colors = Object.values(LimGraphBokeh.tableauColorList());
         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);
            for (let i = 0; i < colMeta.length; i++) {
               let item = document.createElement('span')
               item.className = 'lim-item';
               let glyph = document.createElement('span');
               glyph.className = 'lim-glyph';
               glyph.style.backgroundColor = colors[i % colors.length];
               item.appendChild(glyph);
               let label = document.createElement('span');
               label.className = 'lim-label';
               label.innerText = colMeta[i]?.title;
               item.appendChild(label);
               this.legend.appendChild(item);
            }
         }

         const barWidth = Math.max(0, Math.min(this.#barWidth, 1));
         const size = this.wellContentSize;
         const maxsize = size - 1;
         const margin = Math.round(size * 0.1);
         const content = size - 2 * margin;
         const width = (barWidth * content);
         const first = (content - width) / 2;
         const x = margin + first;

         this.populateTable((cell, wellName) => {
            let bars = [];
            let y = size;
            let sum_val = 0;
            const row = wells[wellName] ?? -1;
            if (row < 0)
               return;
            for (let i = 0; i < colData.length; i++) {
               const val = colData[i][row];
               if (val === null || isNaN(val))
                  continue;
               const y0 = Math.max(min, Math.min(max, sum_val));
               const y1 = Math.max(min, Math.min(max, val + sum_val));
               const yHeight = y1 - y0;
               sum_val += val;
               if (yHeight <= 0)
                  continue;
               const height = Math.round(size * yHeight / rng);
               y -= height;
               bars.push(`<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${colors[i % colors.length]}" outline="none" />`);
            }
            if (0 === bars.length)
               return;

            cell.innerHTML = `<svg height="${size}" width="${size}">${bars.join('\n')}</svg>`;
            cell.className = 'selenabled';
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No data: zero columns checked";
      }
      return true;
   }

   get dataColIds() {
      return this.#dataColIds;
   }

   set dataColIds(val) {
      if (this.#dataColIds === val)
         return;
      this.#dataColIds = val;
      this.dataOptionValueChanged.emit(this);
      this.recreate();
   }
}

/*___________________________________________________________________________*/
class LimPlateViewBoxplot extends LimWellplateTableBase {
   #dataColId

   #dataDefaultFeature
   #dataAllowedFeatures
   #dataForbiddenFeatures
   #dataFeatureMap
   #dataMinimum
   #dataMaximum

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-boxplot');
      this.table.classList.add("lim-wellplate-table-boxplot");

      this.name = pars?.name || "Boxplot";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/boxplot.svg";

      this.#dataColId = pars?.dataColId ? pars.dataColId : null;
      this.#dataDefaultFeature = pars?.dataDefaultFeature ? pars.dataDefaultFeature : null;
      this.#dataAllowedFeatures = pars?.dataAllowedFeatures ? pars.dataAllowedFeatures : null;
      this.#dataForbiddenFeatures = pars?.dataForbiddenFeatures ? pars.dataForbiddenFeatures : null;
      this.#dataFeatureMap = new Map();
      this.#dataMinimum = LimGraphBokeh.tryNumber(pars?.dataMinimum);
      this.#dataMaximum = LimGraphBokeh.tryNumber(pars?.dataMaximum);

      if (pars?.dataOptionVisible ?? true) {
         this.optionList.set("data", {
            type: "selection",
            title: "Data"
         });
      }

      Object.defineProperties(this, {
         dataOptionValues: {
            get() { return [...this.#dataFeatureMap.entries()]; }
         },
         dataOptionValuesChanged: {
            value: new LimSignal(pars?.dataOptionValuesChanged ? [pars.dataOptionValuesChanged] : []),
            writable: false
         },
         dataOptionValue: {
            get() { return this.dataColId; },
            set(val) { this.dataColId = val; }
         },
         dataOptionValueChanged: {
            value: new LimSignal(pars?.dataOptionValueChanged ? [pars.dataOptionValueChanged] : []),
            writable: false
         }
      });
   }

   get colIdList() {
      const td = this.tableData;

      const dataFeatureMap = new Map();
      const allowedFeatures = this.#dataAllowedFeatures ? td.matchColsFulltext(this.#dataAllowedFeatures) : [...limRange(td.colCount)];
      const forbiddenFeatures = this.#dataForbiddenFeatures ? td.matchColsFulltext(this.#dataForbiddenFeatures) : [];
      for (let i = 0; i < td.colCount; i++) {
         const meta = td.colMetadataAt(i);
         if ((meta?.hidden ?? false) === false && allowedFeatures.includes(i) && !forbiddenFeatures.includes(i) && meta?.jsonObject === "boxplot") {
            dataFeatureMap.set(td.colIdAt(i), td.colTitleAt(i));
         }
      }

      if (JSON.stringify([...this.#dataFeatureMap]) !== JSON.stringify([...dataFeatureMap])) {
         this.#dataFeatureMap = dataFeatureMap;
         this.dataOptionValuesChanged.emit(this);
      }

      let dataColIdChanged = false;
      if (![...this.#dataFeatureMap.keys()].includes(this.#dataColId) && this.#dataDefaultFeature) {
         this.#dataColId = td.colIdAt(td.matchColsFulltext(this.#dataDefaultFeature)[0] ?? -1);
         dataColIdChanged = true;
      }

      if (![...this.#dataFeatureMap.keys()].includes(this.#dataColId)) {
         const vals = [...this.#dataFeatureMap.keys()];
         this.#dataColId = vals.find(item => item[0] != '_') ?? "";
         dataColIdChanged = true;
      }

      if (dataColIdChanged)
         this.dataOptionValueChanged.emit(this);

      return this.#dataColId;
   }

   updateData() {
      const td = this.tableData;
      let wells = this.wellToRowsMap();
      if (this.#dataColId) {
         const colData = td.colData(this.#dataColId);
         const meta = td.colMetadata(this.#dataColId);
         const data = colData.map(item => item ? JSON.parse(item) : null).map(item => item?.type === "boxplot" && item?.data ? item.data : null);

         const filteredData = data.filter(item => !!item && Number.isFinite(item.minimum) && Number.isFinite(item.maximum));
         const min = this.#dataMinimum ?? meta?.globalRange?.min ?? Math.min(...filteredData.map(item => item.lower));
         const max = this.#dataMaximum ?? meta?.globalRange?.max ?? Math.max(...filteredData.map(item => item.upper));
         const rng = max - min;

         const median_color = "orange"
         const well_w = this.wellContentSize;
         const well_h = this.wellContentSize;

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);

            let item = document.createElement('span')
            item.className = 'lim-item';
            let glyph = document.createElement('span');
            glyph.className = 'lim-glyph';
            glyph.innerHTML = `<svg width="16" height="9"><line x1="0" y1="3" x2="16" y2="3" class="median" /></svg>`;
            item.appendChild(glyph);
            let label = document.createElement('span');
            label.className = 'lim-label';
            label.innerText = 'median';
            item.appendChild(label);
            this.legend.appendChild(item);

            item = document.createElement('span')
            item.className = 'lim-item';
            glyph = document.createElement('span');
            glyph.className = 'lim-glyph';
            glyph.innerHTML = `<svg width="16" height="9"><rect width="13" height="5" class="iqr" /></svg>`;
            item.appendChild(glyph);
            label = document.createElement('span');
            label.className = 'lim-label';
            label.innerText = 'Q1-Q3';
            item.appendChild(label);
            this.legend.appendChild(item);

            item = document.createElement('span')
            item.className = 'lim-item';
            glyph = document.createElement('span');
            glyph.className = 'lim-glyph';
            glyph.innerHTML = `<svg width="16" height="9">
               <line x1="3" y1="0" x2="13" y2="0" class="limit" />
               <line x1="8" y1="0" x2="8" y2="7" class="range" />
               <line x1="3" y1="7" x2="13" y2="7" class="limit" />
               </svg>`;
            item.appendChild(glyph);
            label = document.createElement('span');
            label.className = 'lim-label';
            label.innerText = '1.5|Q1-Q3|';
            item.appendChild(label);
            this.legend.appendChild(item);

            item = document.createElement('span')
            item.className = 'lim-item';
            glyph = document.createElement('span');
            glyph.className = 'lim-glyph';
            glyph.innerHTML = `<svg width="16" height="9"><circle cx="8" cy="3" r="2" class="outlier" /></svg>`;
            item.appendChild(glyph);
            label = document.createElement('span');
            label.className = 'lim-label';
            label.innerText = 'outliers';
            item.appendChild(label);
            this.legend.appendChild(item);

         }

         const maxx = well_w - 1;
         const maxy = well_h - 1;
         const halfx = maxx / 2;
         const thirdx = maxx / 3;
         const sixthx = maxx / 6;
         const tvelwethx = maxx / 12;
         this.populateTable((cell, wellName) => {
            const row = wells[wellName] ?? -1;
            if (row < 0)
               return;
            const obj = data[row];
            if (!obj)
               return;

            const lo = 1 - (obj.lower - min) / rng;
            const q1 = 1 - (obj.Q1 - min) / rng;
            const q2 = 1 - (obj.median - min) / rng;
            const q3 = 1 - (obj.Q3 - min) / rng;
            const hi = 1 - (obj.upper - min) / rng;

            let lastY = hi;
            let outliers = [];
            for (let pt of obj?.outliers?.upper.filter(item => item < max)) {
               const y = 1 - (pt - min) / rng;
               if (0.1 < lastY - y) {
                  outliers.push(`<circle cx="${halfx}" cy="${maxy * y}" r="2" class="outlier" />`);
                  lastY = y;
               }
            }

            lastY = lo;
            for (let pt of obj?.outliers?.lower.filter(item => min < item).reverse()) {
               const y = 1 - (pt - min) / rng;
               if (0.1 < y - lastY) {
                  outliers.push(`<circle cx="${halfx}" cy="${maxy * y}" r="2" class="outlier" />`);
                  lastY = y;
               }
            }

            cell.innerHTML = `
            <div style="top:${-well_h/2}px;width:${well_w};height:${well_h};position:relative;">
               <svg viewbox="0 0 ${well_w} ${well_h}" style="top:0;left:0;width:${well_w};height:${well_h};position:absolute;">
                  <line x1="${halfx - tvelwethx}" y1="${maxy * lo}" x2="${halfx + tvelwethx}" y2="${maxy * lo}" class="limit" />
                  <line x1="${halfx}" y1="${maxy * lo}" x2="${halfx}" y2="${maxy * q1}" class="range" />
                  <rect x="${thirdx}" y="${maxy * q3}" width="${thirdx}" height="${maxy * (q1 - q3)}" class="iqr"/>
                  <line x1="${halfx - sixthx}" y1="${maxy * q2}" x2="${halfx + sixthx}" y2="${maxy * q2}" class="median" />
                  <line x1="${halfx}" y1="${maxy * q3}" x2="${halfx}" y2="${maxy * hi}" class="range" />
                  <line x1="${halfx - tvelwethx}" y1="${maxy * hi}" x2="${halfx + tvelwethx}" y2="${maxy * hi}" class="limit" />
                  ${outliers.join('\n')}
               </svg>
            </div>`;

            cell.className = 'selenabled';
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No boxplot data: select boxplot column!";
         if (this.#dataFeatureMap?.size) {
            this.errorMessage = "No boxplot data: select boxplot column!";
         } else {
            this.errorMessage = "No boxplot columns!";
         }
      }
      return true;
   }

   get dataColId() {
      return this.#dataColId;
   }

   set dataColId(val) {
      if (this.#dataColId === val)
         return;
      this.#dataColId = val;
      this.dataOptionValueChanged.emit(this);
      this.recreate();
   }
}

/*___________________________________________________________________________*/
class LimPlateViewDosing extends LimWellplateTableBase {
   #logColors

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-dosing');
      this.table.classList.add("lim-wellplate-table-dosing");

      this.name = pars?.name || "Dosing";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/treatment.svg";

      this.#logColors = true;

      this.textVisible = pars?.showTextOption ?? true;
      this.createOptionProperties("textVisible", "Show  numbers in wells", { hidden: !(pars?.showTextOptionVisible ?? true), iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/number_count_show.svg" })
   }

   get colIdList() {
      let ret = [];
      const td = this.tableData;
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'Dose'));
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'DoseGroup'));
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'DoseCompound'));
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'LabelControl'));
      return ret.filter(item => (0 <= item)).map(item => td.colIdAt(item));
   }

   updateData() {
      const { color_gray_1, darken_by } = this.rootStyles();
      const darken = (c) => {
         const ret = d3.lab(c);
         ret.l *= darken_by;
         return ret.formatHex8();
      };

      let wells = this.wellToRowsMap();
      const doseIndex = this.tableData.colMetadataList.findIndex(item => item.feature === 'Dose');
      if (0 <= doseIndex) {
         const data = this.tableData.colDataAt(doseIndex);
         const meta = this.tableData.colMetadataAt(doseIndex);
         const title = this.tableData.colTitleAt(doseIndex);
         const unit = this.tableData.colUnitAt(doseIndex);
         const fmtValue = LimTableData.makePrintFunction(meta);

         let logColors = this.#logColors;
         let [min, max] = [0, 0];
         if (this.#logColors) {
            const safeData = data.filter(item => 0 < item);
            if (1 < safeData.length) {
               min = Math.log10(Math.min(...safeData));
               max = Math.log10(Math.max(...safeData));
               if (max - min <= 1)
                  logColors = false;
            }
            else
               logColors = false;
         }

         if (!logColors) {
            min = Math.min(...data);
            max = Math.max(...data);
         }

         const rng = max - min;

         const doseGroupIndex = this.tableData.colMetadataList.findIndex(item => item.feature === 'DoseGroup');
         const doseGroupData = 0 <= doseGroupIndex ? this.tableData.colDataAt(doseGroupIndex) : null;
         const doseCompoundIndex = this.tableData.colMetadataList.findIndex(item => item.feature === 'DoseCompound');
         const doseCompoundData = 0 <= doseCompoundIndex ? this.tableData.colDataAt(doseCompoundIndex) : null;

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);
         }

         const concentrationColors = { "-": "purple" };
         for (let def of meta.definitions) {
            if (typeof def === "object" && def.hasOwnProperty('color') && def.hasOwnProperty('group') && def.hasOwnProperty('compound')) {
               const col = darken(def.color);
               if (0 <= doseGroupIndex || 0 <= doseCompoundIndex)
                  concentrationColors[`${0 <= doseGroupIndex ? def.group : ""}-${0 <= doseCompoundIndex ? def.compound : ""}`] = col;

               if (this.legend) {
                  const item = document.createElement('span')
                  item.className = 'lim-item';
                  const glyph = document.createElement('span');
                  glyph.className = 'lim-glyph';
                  glyph.style.backgroundColor = col;
                  item.appendChild(glyph);
                  const label = document.createElement('span');
                  label.className = 'lim-label';
                  label.innerText = 0 < def?.compound?.length && 0 < def?.group?.length
                     ? `${title} of ${def.compound.trim()}, ${def.group.trim()} [${unit}]`
                     : `${title} of ${def.compound.trim()}${def.group.trim()} [${unit}]`;
                  item.appendChild(label);
                  this.legend.appendChild(item);
               }
            }
         }

         const controlColors = { positive: "blue", negative: "red" };
         const controlIndex = this.tableData.colMetadataList.findIndex(item => item.feature === 'LabelControl');
         const controlData = 0 <= controlIndex ? this.tableData.colDataAt(controlIndex) : null;
         if (controlData) {
            const m = this.tableData.colMetadataAt(controlIndex);
            for (let def of m.definitions) {
               const col = (9 === def.color.length && def.color[0] === '#') ? `#${def.color.substring(3)}` : def.color;
               controlColors[def.value] = col;
               if (this.legend) {
                  const item = document.createElement('span')
                  item.className = 'lim-item';
                  const glyph = document.createElement('span');
                  glyph.className = 'lim-glyph';
                  glyph.innerHTML = `<svg width="16" height="16"><polygon points="0,0 6,0 0,6" fill="${col}" /></svg>`;
                  item.appendChild(glyph);
                  const label = document.createElement('span');
                  label.className = 'lim-label';
                  label.innerText = def.name;
                  item.appendChild(label);
                  this.legend.appendChild(item);
               }
            }
         }

         const size = this.wellContentSize;
         const glyphSize = Math.round(size / 4);
         this.populateTable((cell, wellName) => {
            const row = wells[wellName] ?? -1;
            if (row < 0)
               return;

            const container = document.createElement('div');
            container.className = 'lim-position-relative selenabled';

            const concentration = data[row];
            if (Number.isFinite(concentration)) {
               const group = doseGroupData?.[row] ?? "";
               const compound = doseCompoundData?.[row] ?? "";
               const value = logColors ? (0 < concentration ? (Math.log10(concentration) - min) / rng : 0) : (concentration - min) / rng;
               const interpolator = d3.interpolateRgb(color_gray_1, concentrationColors[`${group}-${compound}`]);

               container.style.backgroundColor = interpolator(value);

               const label = document.createElement('div');
               label.className = 'lim-flex-centered lim-fill text-on-color';
               label.style.width = `${this.wellContentSize}px`;
               label.style.height = `${this.wellContentSize}px`;
               if (this.textVisible)
                  label.innerText = fmtValue(concentration);
               container.appendChild(label);
            }
            else {
               const label = document.createElement('div');
               label.className = 'lim-flex-centered lim-fill text-on-color';
               label.style.width = `${this.wellContentSize}px`;
               label.style.height = `${this.wellContentSize}px`;
               container.appendChild(label);
            }

            if (controlData && controlColors.hasOwnProperty(controlData[row])) {
               const control = controlData[row];
               const glyph = document.createElement('div');
               glyph.className = 'lim-glyph lim-position-absolute lim-position-top lim-position-left';
               glyph.innerHTML = `<svg width="${glyphSize + 1}" height="${glyphSize + 1}"><polygon points="0,0 ${glyphSize},0 0,${glyphSize}" fill="${controlColors[control]}" /></svg>`;

               container.appendChild(glyph);
            }

            cell.appendChild(container);
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No dosing data: column with concentrations missing!";
      }
      return true;
   }
}

/*___________________________________________________________________________*/
class LimPlateViewHeatmap extends LimWellplateTableBase {
   #dataColId
   #dataDefaultFeature
   #dataAllowedFeatures
   #dataForbiddenFeatures
   #dataFeatureMap
   #dataLogColors
   #dataMinimum
   #dataMaximum

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-heatmap');
      this.table.classList.add("lim-wellplate-table-heatmap");

      this.name = pars?.name || "Heatmap";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/heatmap.svg";

      this.#dataColId = pars?.dataColId ? pars.dataColId : null;
      this.#dataDefaultFeature = pars?.dataDefaultFeature ? pars.dataDefaultFeature : null;
      this.#dataAllowedFeatures = pars?.dataAllowedFeatures ? pars.dataAllowedFeatures : null;
      this.#dataForbiddenFeatures = pars?.dataForbiddenFeatures ? pars.dataForbiddenFeatures : null;
      this.#dataLogColors = !!pars?.dataLogColors;
      this.#dataFeatureMap = new Map();
      this.#dataMinimum = LimGraphBokeh.tryNumber(pars?.dataMinimum);
      this.#dataMaximum = LimGraphBokeh.tryNumber(pars?.dataMaximum);

      if (pars?.dataOptionVisible ?? true) {
         this.optionList.set("data", {
            type: "selection",
            title: "Data"/*,
            iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/graph_axis_x.svg"*/
         });
      }

      Object.defineProperties(this, {
         dataOptionValues: {
            get() { return [...this.#dataFeatureMap.entries()]; }
         },
         dataOptionValuesChanged: {
            value: new LimSignal(pars?.dataOptionValuesChanged ? [pars.dataOptionValuesChanged] : []),
            writable: false
         },
         dataOptionValue: {
            get() { return this.dataColId; },
            set(val) { this.dataColId = val; }
         },
         dataOptionValueChanged: {
            value: new LimSignal(pars?.dataOptionValueChanged ? [pars.dataOptionValueChanged] : []),
            writable: false
         }
      });

      this.textVisible = pars?.showTextOption ?? true;
      this.createOptionProperties("textVisible", "Show numbers in wells", { hidden: !(pars?.showTextOptionVisible ?? true), iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/number_count_show.svg" })
   }

   get colIdList() {
      const td = this.tableData;

      const dataFeatureMap = this.makeNumericFeatureMap(this.#dataAllowedFeatures, this.#dataForbiddenFeatures);
      if (JSON.stringify([...this.#dataFeatureMap]) !== JSON.stringify([...dataFeatureMap])) {
         this.#dataFeatureMap = dataFeatureMap;
         this.dataOptionValuesChanged.emit(this);
      }

      let dataColIdChanged = false;
      if (![...this.#dataFeatureMap.keys()].includes(this.#dataColId) && this.#dataDefaultFeature) {
         this.#dataColId = td.colIdAt(td.matchColsFulltext(this.#dataDefaultFeature)[0] ?? -1);
         dataColIdChanged = true;
      }

      if (![...this.#dataFeatureMap.keys()].includes(this.#dataColId)) {
         const vals = [...this.#dataFeatureMap.keys()];
         this.#dataColId = vals.find(item => item[0] != '_') ?? "";
         dataColIdChanged = true;
      }

      if (dataColIdChanged)
         this.dataOptionValueChanged.emit(this);

      return this.#dataColId;
   }

   updateData() {
      const td = this.tableData;
      let wells = this.wellToRowsMap();
      if (this.#dataColId) {
         const data = td.colData(this.#dataColId);
         const meta = td.colMetadata(this.#dataColId);

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);
         }

         if (meta.decltype === "double" || meta.decltype === "int") {
            const filtData = this.#dataLogColors ? data.filter(item => Number.isFinite(item) && 0 < item) : data.filter(item => Number.isFinite(item));
            let min = 0, max = 0, logRange = 0;
            if (this.#dataLogColors) {
               min = (this.#dataMinimum != null ? Math.log10(this.#dataMinimum) : null) ?? meta?.globalRange?.logMin ?? Math.log10(Math.min(...filtData));
               max = (this.#dataMaximum != null ? Math.log10(this.#dataMaximum) : null) ?? meta?.globalRange?.logMax ?? Math.log10(Math.max(...filtData));
               logRange = Math.max(0, min < max ? Math.log10(10 ** max - 10 ** min) : 0);
            }
            else {
               min = this.#dataMinimum ?? meta?.globalRange?.min ?? Math.min(...filtData);
               max = this.#dataMaximum ?? meta?.globalRange?.max ?? Math.max(...filtData);
            }

            const rng = max > min ? max - min : 1;
            const low_color = "purple";
            const high_color = "orange";
            const interpolator = max > min ? d3.interpolateRgb(low_color, high_color) : d3.interpolateRgb(low_color, low_color);
            const prec = this.#dataLogColors ? Math.max(0, Math.ceil(4 - logRange)) : Math.max(0, Math.ceil(3 - logRange));
            const fmtValue = LimTableData.makePrintFunction(meta);
            const makeColor = (val) => {
               return interpolator(val);
            };

            function measureText(text, font) {
               const canvas = measureText.canvas || (measureText.canvas = document.createElement("canvas"));
               const context = canvas.getContext("2d");
               context.font = font;
               const metrics = context.measureText(text);
               return metrics.width;
            }

            if (this.legend) {
               const width = 300;
               const item = document.createElement('span')
               const tickFmtValue = meta.decltype === "double" ? LimTableData.printCellDoubleFlt : LimTableData.printCellInteger;
               item.className = 'lim-item';

               let ticks = [];
               let tickLabels = [];
               if (this.#dataLogColors) {
                  for (let tick = Math.ceil(min); tick <= Math.floor(max); tick++) {
                     const x = (tick - min) / rng;
                     ticks.push(`<line x1="${x * width}" y1="5" x2="${x * width}" y2="15" class="tick" />`);
                     tickLabels.push(`<text x="${x * width}" y="28" text-anchor="middle">${10 ** tick}</text>`);
                  }
               }
               else if (0 < rng) {
                  const optTickCount = 4;
                  const optStep = rng / (optTickCount + 1);
                  const exp = 10 ** Math.floor(Math.log10(optStep));
                  const step = [10 * exp, 5 * exp, 2.5 * exp, exp, exp / 2, exp / 4].map(item => [item, (optStep - item) ** 2]).reduce((prev, curr) => prev[1] < curr[1] ? prev : curr)[0];
                  const tickPrec = 1 <= step ? 0 : prec;

                  const stepCount = Math.ceil(rng/step);
                  const stepMin = Math.ceil(min / step) * step;
                  const stepMax = stepMin + Math.floor((max -stepMin) / step) * step;
                  const stepMinPos = (stepMin - min) / rng * width;
                  const stepMaxPos = (stepMax - min) / rng * width;
                  const stepSpace = (stepMaxPos - stepMinPos) / (stepCount - 1) - 4;
                  const textMaxSize = Math.max(measureText(tickFmtValue(stepMin, tickPrec), "11px Tahoma"), measureText(tickFmtValue(stepMax, tickPrec), "11px Tahoma"));
                  const tickFontSize = textMaxSize > stepSpace ? String(11*stepSpace / textMaxSize) + "px" : "11px";
                  for (let i = stepMin; i <= max; i += step) {
                     const x = (i - min) / rng;
                     ticks.push(`<line x1="${x * width}" y1="5" x2="${x * width}" y2="15" class="tick" />`);
                     tickLabels.push(`<text x="${x * width}" style="font-size: ${tickFontSize}" y="28" text-anchor="middle">${tickFmtValue(i, tickPrec)}</text>`);
                  }
               }

               const single_tick = `<line x1="${width / 2.0}" y1="5" x2="${width / 2.0}" y2="15" class="tick" />`
               const single_value = `<text x="${width / 2.0}" y="28" text-anchor="middle">${tickFmtValue(min, prec)}</text>`

               const colorbar = document.createElement('span');
               colorbar.className = 'lim-label';
               colorbar.innerHTML = `
                  <svg height="30" width="${width}" style="overflow:visible" viewBox="0 0 ${width} 30">
                     <defs>
                        <linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
                           <stop offset="  0%" style="stop-color:${makeColor(0.00)};" />
                           <stop offset=" 25%" style="stop-color:${makeColor(0.25)};" />
                           <stop offset=" 50%" style="stop-color:${makeColor(0.50)};" />
                           <stop offset=" 75%" style="stop-color:${makeColor(0.75)};" />
                           <stop offset="100%" style="stop-color:${makeColor(1.00)};" />
                        </linearGradient>
                     </defs>
                     <rect width="${width}" height="10" fill="url(#grad1)" class="lim-glyph" />
                     ${max > min ? ticks.join("\n") : single_tick}
                     ${max > min ? tickLabels.join("\n") : single_value}
                  </svg>`;

               item.appendChild(colorbar);


               this.legend.appendChild(item);
            }

            this.populateTable((cell, wellName) => {
               const row = wells[wellName] ?? -1;
               if (row < 0)
                  return;
               const val = data[row];
               if (!Number.isFinite(val))
                  return;
               if (this.textVisible) {
                  let textValue = fmtValue(val, prec);
                  const cellWidthPx = Number(cell.style.width.replace('px', '')) - 6;
                  if (Number.isFinite(cellWidthPx)) {
                     const textWidthPx = measureText(textValue, cell.style.fontSize + " Tahoma");
                     if (textWidthPx > cellWidthPx) {
                        const fontSizeOld = Number(cell.style.fontSize.replace("px", ''));
                        cell.style.fontSize = String(cellWidthPx / textWidthPx * fontSizeOld) + "px";
                     }
                  }
                  cell.innerText = textValue;
               }
               cell.className = 'selenabled text-on-color';
               cell.style.backgroundColor = makeColor(this.#dataLogColors ? (0 < val ? (Math.log10(val) - min) / rng : 0) : ((val - min) / rng));
            });
         }
         else {
            const fmtValue = LimTableData.makePrintFunction(meta);
            this.populateTable((cell, wellName) => {
               const row = wells[wellName] ?? -1;
               if (row < 0)
                  return;
               cell.innerText = fmtValue(data[row]);
            });
         }
      }
      else {
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No data: select data column!";
      }
   }

   get dataColId() {
      return this.#dataColId;
   }

   set dataColId(val) {
      if (this.#dataColId === val)
         return;
      this.#dataColId = val;
      this.dataOptionValueChanged.emit(this);
      this.recreate();
   }
}

/*___________________________________________________________________________*/
class LimPlateViewImage extends LimWellplateTableBase {
   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-image');
      this.table.classList.add("lim-wellplate-table-image");

      this.name = pars?.name || "Image";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/image.svg";

      this.colChannels = [];
      this.selectedColChannel = 0;
      this.createSelectionProperties("selectedColChannel", "Selected color channel", "colChannels", { hidden: !(pars?.colorChannelSelection ?? true) })

      this.binLayers = [];
      this.selectedBinLayers = null;
      this.createMultiSelectionProperties("selectedBinLayers", "Selected binary layers", "binLayers", { hidden: !(pars?.binaryLayerSelection ?? true), enabled: () => this?.binLayers?.length ?? false })

      this.scaleVisible = true;
      this.createOptionProperties("scaleVisible", "Scale visible", { iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/scale_hor16.svg" })
   }

   updateSelections(colChannels, binLayers) {
      if (typeof this.selectedBinLayers === "undefined") {
         return;
      }

      if (JSON.stringify(this.colChannels) !== JSON.stringify(colChannels)) {
         this.colChannels = colChannels;
         this.selectedColChannelOptionValuesChanged.emit(this);
      }

      if (this?.colChannels?.length && !this.colChannels.map(item => item[0]).includes(this.selectedColChannel)) {
         this.selectedColChannel = this?.colChannels[0];
         this.selectedColChannelOptionValueChanged.emit(this);
      }

      if (this.selectedBinLayers === null && 0 == (binLayers?.length ?? 0))
         return;

      if (JSON.stringify(this.binLayers) !== JSON.stringify(binLayers)) {
         this.binLayers = binLayers;
         this.selectedBinLayersOptionValuesChanged.emit(this);
         this.selectedBinLayersOptionEnabledChanged.emit(this);
      }

      if (this.selectedBinLayers === null) {
         this.selectedBinLayers = [] //this.binLayers.map(item => item[0]);
         this.selectedBinLayersOptionValueChanged.emit(this);
      }
      else {
         const available = this.binLayers.map(item => item[0]);
         const sel = this.selectedBinLayers.filter(item => available.includes(item));
         if (JSON.stringify(this.selectedBinLayers) !== JSON.stringify(sel)) {
            this.selectedBinLayers = sel;
            this.selectedBinLayersOptionValueChanged.emit(this);
         }
      }
   }

   get colIdList() {
      let ret = [];
      const td = this.tableData;
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'WellThumbnail' || item.feature === 'Image'));
      return ret.filter(item => (0 <= item)).map(item => td.colIdAt(item));
   }

   updateData() {
      const td = this.tableData;
      let wells = this.wellToRowsMap();
      const thumbnailIndex = this.tableData.colMetadataList.findIndex(item => item.feature === 'WellThumbnail' || item.feature === 'Image');
      if (0 <= thumbnailIndex) {
         const data = this.tableData.colDataAt(thumbnailIndex);
         const meta = this.tableData.colMetadataAt(thumbnailIndex);
         const images = meta?.images;
         const parser = new DOMParser();
         const errorFn = (error) => `<span class="lim-error">${error}</span>`;

         const colChannels = images?.map?.((item, index) => item.type == "color-image" ? [index, item.name] : [-1, ""])?.filter?.(item => (0 <= item[0])) ?? [];
         const binLayers = images?.map?.((item, index) => item.type == "binary-overlay" ? [index, item.name] : [-1, ""])?.filter?.(item => (0 <= item[0])) ?? [];
         const scales = this.scaleVisible ? (images?.map?.((item, index) => item.type == "scale" ? index : -1)?.filter?.(item => (0 <= item)) ?? []) : [];
         this.updateSelections(colChannels, binLayers);

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);

            if (meta?.imageSizeUmX && meta?.imageSizeUmY) {
               const name = images?.[this?.selectedColChannel]?.name;
               const item = document.createElement('span')
               item.className = 'lim-item';
               const glyph = document.createElement('span');
               glyph.className = 'lim-glyph';
               item.appendChild(glyph);
               const label = document.createElement('span');
               label.className = 'lim-label';
               if (scales?.length && images?.[scales[0]]?.label)
                  label.innerText = `Image${name === "All" ? "" : " - " + name} (scale = ${images[scales[0]].label})`;
               else
                  label.innerText = `Image${name === "All" ? "" : " - " + name} (center ${Math.round(Math.min(meta.imageSizeUmX, meta.imageSizeUmY))} \u00B5m)`;
               item.appendChild(label);
               this.legend.appendChild(item);
            }

            if (images && 0 < (this.selectedBinLayers?.length ?? 0)) {
               for (let ibin of this.selectedBinLayers) {
                  const bin = images[ibin];
                  const item = document.createElement('span')
                  item.className = 'lim-item';
                  const glyph = document.createElement('span');
                  glyph.className = 'lim-glyph';
                  glyph.style.backgroundColor = bin.color;
                  item.appendChild(glyph);
                  const label = document.createElement('span');
                  label.className = 'lim-label';
                  label.innerText = bin.name;
                  item.appendChild(label);
                  this.legend.appendChild(item);
               }
            }
         }

         const selChannels = this?.selectedColChannel ?? 0;
         const selBinLayers = this?.selectedBinLayers ?? [];
         const validdataindexes = data.map((item, index) => item ? index : -1).filter(item => (0 <= item));
         const lastvalidrowindex = 0 < validdataindexes.length ? validdataindexes[validdataindexes.length - 1] : -1;
         this.populateTable((cell, wellName) => {
            const row = wells[wellName] ?? -1;
            if (row < 0)
               return;
            const value = data[row];
            if (meta?.jsonObject === "image") {
               try {
                  const obj = JSON.parse(value);
                  if (obj) {
                     cell.innerHTML = obj?.src ? `<img src="${obj.src}"/>` : `<img src="data:${obj.data.type},${obj.data.base64}" draggable="false"/>`;
                  }
               }
               catch (e) {
                  console.log(e);
                  cell.innerHTML = errorFn('Error');
               }
            }
            else if (meta?.jsonObject === "multi-image") {
               try {
                  const obj = JSON.parse(value);
                  if (obj && Array.isArray(obj.data) && 0 <= selChannels && selChannels < obj.data.length) {
                     const imgSrc = obj.data[selChannels].data?.src ? `data:${obj.data[selChannels].data.src},${obj.data[selChannels].data.base64}` : `data:${obj.data[selChannels].data.type},${obj.data[selChannels].data.base64}`
                     const color = `<img class="lim-image-color" style="width:${this.wellContentSize}px;height:${this.wellContentSize}px;" src="${imgSrc}" draggable="false" />`;
                     const binaries = selBinLayers.length ? selBinLayers.map(item => obj.data[item].data).reduce((p, c) => p + c) : "";
                     const scale = (row == lastvalidrowindex) && scales.length ? scales.map(item => obj.data[item].data).reduce((p, c) => p + c) : "";
                     cell.innerHTML = `<div class="lim-multi-image selenabled" style="width:${this.wellContentSize}px;height:${this.wellContentSize}px;">${color}${binaries}${scale}</div>`;
                  }
               }
               catch (e) {
                  console.log(e);
                  cell.innerHTML = errorFn('Error');
               }
            }
         });
      }
      else {
         this.updateSelections([], []);
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No data: thmbnail column missing!";
      }
      return true;
   }
}

/*___________________________________________________________________________*/
class LimPlateViewLabeling extends LimWellplateTableBase {

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-labeling');
      this.table.classList.add("lim-wellplate-table-labeling");

      this.name = pars?.name || "Labeling";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/label.svg";

      this.labels = [];
      this.selectedLabels = null;
      this.createMultiSelectionProperties("selectedLabels", "Selected labels", (() => this?.labels?.map?.(item => [item, item]) ?? []), { hidden: !(pars?.dataOptionVisible ?? true), enabled: () => this?.labels?.length ?? false })

      this.textVisible = pars?.showTextOption ?? true;
      this.createOptionProperties("textVisible", "Show labels in wells", { hidden: !(pars?.showTextOptionVisible ?? true), iconres: "/res/gnr_core_gui/CoreGUI/Icons/base/number_count_show.svg" })
   }

   get colIdList() {
      let ret = [];
      const td = this.tableData;
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'LabelDetection'));
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'LabelControl'));
      ret.push(this.tableData.colMetadataList.findIndex(item => item.feature === 'Label'));
      return ret.filter(item => (0 <= item)).map(item => td.colIdAt(item));
   }

   updateData() {
      const td = this.tableData;
      const { darken_by } = this.rootStyles();
      const darken = (c) => {
         const ret = d3.lab(c);
         ret.l *= darken_by;
         return ret.formatHex8();
      };

      let labels = [];
      const radii = new Map();
      const colors = new Map();
      const nameToValue = new Map();
      const detectionColIndex = this.tableData.colMetadataList.findIndex(item => item.feature === 'LabelDetection');
      const labelColIndexes = [
         this.tableData.colMetadataList.findIndex(item => item.feature === 'LabelControl'),
         this.tableData.colMetadataList.findIndex(item => item.feature === 'Label'),
         detectionColIndex
      ].filter(item => 0 <= item);
      for (let colIndex of labelColIndexes) {
         const meta = this.tableData.colMetadataAt(colIndex);
         for (let def of meta.definitions) {
            labels.push(def.name);
            nameToValue.set(def.name, def?.value ?? def.name);
            colors.set(def?.value ?? def.name, darken(9 === def.color.length && def.color[0] === '#' ? `#${def.color.substring(3)}` : def.color));
            radii.set(def.name, colIndex == detectionColIndex ? 'lim-glyph circle' : 'lim-glyph');
         }
      }

      if (JSON.stringify(this.labels) !== JSON.stringify(labels)) {
         this.labels = labels;
         this.selectedLabelsOptionValuesChanged.emit(this);
         this.selectedLabelsOptionEnabledChanged.emit(this);
      }

      if (this.selectedLabels === null) {
         this.selectedLabels = [...this.labels];
         this.selectedLabelsOptionValueChanged.emit(this);
      }
      else {
         const sel = this.selectedLabels.filter(item => this.labels.includes(item));
         if (JSON.stringify(this.selectedLabels) !== JSON.stringify(sel)) {
            this.selectedLabels = sel;
            this.selectedLabelsOptionValueChanged.emit(this);
         }
      }

      let wells = this.wellToRowsMap();
      if (labelColIndexes.length) {
         let mainData = new Array(this.tableData.rowCount);
         let auxData = new Array(this.tableData.rowCount);
         for (let i = 0; i < this.tableData.rowCount; i++) {
            mainData[i] = [];
            auxData[i] = [];
         }
         const sel = this.selectedLabels.map(item => nameToValue.get(item));
         for (let i = 0; i < labelColIndexes.length; i++) {
            let data = labelColIndexes[i] == detectionColIndex ? auxData : mainData;
            this.tableData.colDataAt(labelColIndexes[i]).forEach((item, i) => {
               if (typeof item === 'string')
                  item.split(',').map(item => item.trim()).filter(item => sel.includes(item)).forEach(elem => data[i].push(elem));
            });
         }

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);

            for (let entry of nameToValue.entries()) {
               const item = document.createElement('span')
               item.className = 'lim-item';
               const glyph = document.createElement('span');
               glyph.className = radii.get(entry[0]);
               glyph.style.backgroundColor = colors.get(entry[1]);
               item.appendChild(glyph);
               const label = document.createElement('span');
               label.className = 'lim-label';
               label.innerText = entry[0];
               item.appendChild(label);
               this.legend.appendChild(item);
            }
         }

         this.populateTable((cell, wellName) => {
            const row = wells[wellName] ?? -1;
            if (row < 0)
               return;

            const mainVal = mainData[row];
            const auxVal = auxData[row];
            const container = document.createElement('div');
            container.className = 'lim-position-relative selenabled';
            if (1 === mainVal?.length) {
               container.style.backgroundColor = colors.get(mainVal[0]);
            }
            else if (1 < mainVal?.length) {
               let labelColors = []
               const w = 100 / mainVal.length;
               for (let i = 0; i < mainVal.length; i++) {
                  labelColors.push(`${colors.get(mainVal[i])} ${i * w}% ${(i + 1) * w}%`);
               }
               container.style.background = `linear-gradient(to right, ${labelColors.join(", ")})`;
            }

            let aux = null;
            if (auxVal?.length) {
               aux = document.createElement('div');
               aux.className = 'lim-position-absolute';
               aux.style.padding = '4px';
               aux.style.width = `${this.wellContentSize}px`;
               aux.style.height = `${this.wellContentSize}px`;
               aux.style.display = 'flex';
               aux.style.flexDirection = 'row';
               aux.style.flexWrap = 'wrap';

               for (let i = 0; i < auxVal.length; i++) {
                  let circle = document.createElement('div');
                  circle.style.backgroundColor = colors.get(auxVal[i]);
                  circle.style.margin = '1px';
                  circle.style.width = '1.2em';
                  circle.style.height = '1.2em';
                  circle.style.borderRadius = '50%';
                  aux.appendChild(circle);
               }
               container.appendChild(aux);
            }

            const label = document.createElement('div');
            label.className = 'lim-flex-centered lim-fill text-on-color';
            label.style.width = `${this.wellContentSize}px`;
            label.style.height = `${this.wellContentSize}px`;
            if (this.textVisible)
               label.innerText = mainVal.join(', ');
            container.appendChild(label);

            cell.appendChild(container);
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         this.errorMessage = "No data: Control or Labels column missing!";
      }
      return true;
   }
}

/*___________________________________________________________________________*/
class LimPlateViewViolin extends LimWellplateTableBase {
   #dataColId
   #dataDefaultFeature
   #dataAllowedFeatures
   #dataForbiddenFeatures
   #dataFeatureMap

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-violin');
      this.table.classList.add("lim-wellplate-table-violin");

      this.name = pars?.name || "Violin";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/violinplot.svg";

      this.#dataColId = pars?.dataColId ? pars.dataColId : null;
      this.#dataDefaultFeature = pars?.dataDefaultFeature ? pars.dataDefaultFeature : null;
      this.#dataAllowedFeatures = pars?.dataAllowedFeatures ? pars.dataAllowedFeatures : null;
      this.#dataForbiddenFeatures = pars?.dataForbiddenFeatures ? pars.dataForbiddenFeatures : null;
      this.#dataFeatureMap = new Map();

      if (pars?.dataOptionVisible ?? true) {
         this.optionList.set("data", {
            type: "selection",
            title: "Data"
         });
      }

      Object.defineProperties(this, {
         dataOptionValues: {
            get() { return [...this.#dataFeatureMap.entries()]; }
         },
         dataOptionValuesChanged: {
            value: new LimSignal(pars?.dataOptionValuesChanged ? [pars.dataOptionValuesChanged] : []),
            writable: false
         },
         dataOptionValue: {
            get() { return this.dataColId; },
            set(val) { this.dataColId = val; }
         },
         dataOptionValueChanged: {
            value: new LimSignal(pars?.dataOptionValueChanged ? [pars.dataOptionValueChanged] : []),
            writable: false
         }
      });
   }

   get colIdList() {
      const td = this.tableData;

      const dataFeatureMap = new Map();
      const allowedFeatures = this.#dataAllowedFeatures ? td.matchColsFulltext(this.#dataAllowedFeatures) : [...limRange(td.colCount)];
      const forbiddenFeatures = this.#dataForbiddenFeatures ? td.matchColsFulltext(this.#dataForbiddenFeatures) : [];
      for (let i = 0; i < td.colCount; i++) {
         const meta = td.colMetadataAt(i);
         if ((meta?.hidden ?? false) === false && allowedFeatures.includes(i) && !forbiddenFeatures.includes(i) && meta?.jsonObject === "violinPlot") {
            dataFeatureMap.set(td.colIdAt(i), td.colTitleAt(i));
         }
      }

      if (JSON.stringify([...this.#dataFeatureMap]) !== JSON.stringify([...dataFeatureMap])) {
         this.#dataFeatureMap = dataFeatureMap;
         this.dataOptionValuesChanged.emit(this);
      }

      let dataColIdChanged = false;
      if (![...this.#dataFeatureMap.keys()].includes(this.#dataColId) && this.#dataDefaultFeature) {
         this.#dataColId = td.colIdAt(td.matchColsFulltext(this.#dataDefaultFeature)[0] ?? -1);
         dataColIdChanged = true;
      }

      if (![...this.#dataFeatureMap.keys()].includes(this.#dataColId)) {
         const vals = [...this.#dataFeatureMap.keys()];
         this.#dataColId = vals.find(item => item[0] != '_') ?? "";
         dataColIdChanged = true;
      }

      if (dataColIdChanged)
         this.dataOptionValueChanged.emit(this);

      return this.#dataColId;
   }

   updateData() {
      const td = this.tableData;
      let wells = this.wellToRowsMap();
      if (this.#dataColId) {
         const colData = td.colData(this.#dataColId);
         const meta = td.colMetadata(this.#dataColId);
         const data = colData.map(item => item ? JSON.parse(item) : null).map(item => item?.type === "violinplot" && item?.data ? item.data : null);

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);

            let item = document.createElement('span')
            item.className = 'lim-item';
            let glyph = document.createElement('span');
            glyph.className = 'lim-glyph';
            glyph.innerHTML = `<svg width="16" height="9"><rect x="1" y="2" width="14" height="3" class="violin" /></svg>`;
            item.appendChild(glyph);
            let label = document.createElement('span');
            label.className = 'lim-label';
            label.innerText = '1.5|Q1-Q3|';
            item.appendChild(label);
            this.legend.appendChild(item);
         }

         const size = this.wellContentSize;
         const hsize = size / 2;
         const amp = size / 3;

         this.populateTable((cell, wellName) => {
            const row = wells[wellName] ?? -1;
            if (row < 0 || !data[row] || !(0 < data?.[row]?.x?.length && 0 < data?.[row]?.y?.length))
               return;

            let points1 = [];
            let points2 = [];
            const [x, y] = [data[row].x, data[row].y];
            const [xmin, xmax] = [x[0], x[x.length - 1]];
            const len = Math.min(x.length, y.length);
            const ymax = data?.[row]?.yMax ?? Math.max(...y);
            for (let i = 0; i < size; i += 2) {
               const j = Math.floor(i * len / size);
               const _x = size * (x[j] - xmin) / xmax;
               const _y = amp * y[j] / ymax;
               points1.push(`L${_x} ${hsize - _y}`);
               points2.push(`L${_x} ${hsize + _y}`);
            }

            cell.innerHTML = `
            <div style="top:${-size/2}px;width:${size};height:${size};position:relative;">
               <svg viewbox="0 0 ${size} ${size}" style="top:0;left:0;width:${size};height:${size};position:absolute;">
                  <path d="M0 ${hsize} ${[...points1, ...points2.reverse()].join(' ')} Z" transform="translate(${hsize},${hsize}) rotate(-90) translate(${-hsize},${-hsize})" class="violin" />
               </svg>
            </div>`;

            cell.className = 'selenabled';
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         if (this.#dataFeatureMap?.size) {
            this.errorMessage = "No violinplot data: select violinplot column!";
         } else {
            this.errorMessage = "No violinplot columns!";
         }
      }
      return true;
   }

   get dataColId() {
      return this.#dataColId;
   }

   set dataColId(val) {
      if (this.#dataColId === val)
         return;
      this.#dataColId = val;
      this.dataOptionValueChanged.emit(this);
      this.recreate();
   }
}

/*___________________________________________________________________________*/
class LimPlateViewLineChart extends LimWellplateTableBase {
   #dataXColId
   #dataYColId
   #dataYAvailableColIds
   #dataYFeatureMap

   constructor() {
      super();
   }

   initialize(pars, ...args) {
      super.initialize(pars, ...args);

      this.classList.add('lim-plate-view-linechart');
      this.table.classList.add("lim-wellplate-table-linechart");

      this.name = pars?.name || "Linechart";
      this.iconres = pars?.iconres || "/res/gnr_core_gui/CoreGUI/Icons/base/timeplot.svg";

      this.#dataXColId = pars?.dataXFeature ? pars.dataXFeature : null;
      this.#dataYColId = pars?.dataYFeature ? pars.dataYFeature : null;
      this.#dataYAvailableColIds = pars?.dataYAvailableFeatures ? pars.dataYAvailableFeatures : null;
      if (this.#dataYColId && Array.isArray(this.#dataYAvailableColIds) && !this.#dataYAvailableColIds.includes(this.#dataYColId))
         this.#dataYAvailableColIds.push(this.#dataYColId);

      this.#dataYFeatureMap = new Map();

      if (pars?.dataOptionVisible ?? true) {
         this.optionList.set("data", {
            type: "selection",
            title: "Data"
         });
      }

      Object.defineProperties(this, {
         dataOptionValues: {
            get() { return [...this.#dataYFeatureMap.entries()]; }
         },
         dataOptionValuesChanged: {
            value: new LimSignal(pars?.dataOptionValuesChanged ? [pars.dataOptionValuesChanged] : []),
            writable: false
         },
         dataOptionValue: {
            get() { return this.dataYColId; },
            set(val) { this.dataYColId = val; }
         },
         dataOptionValueChanged: {
            value: new LimSignal(pars?.dataOptionValueChanged ? [pars.dataOptionValueChanged] : []),
            writable: false
         }
      });
   }

   get colIdList() {
      const td = this.tableData;
      const dataYFeatureMap = new Map();
      for (let i = 0; i < td.colCount; i++) {
         if (this.#dataYAvailableColIds && this.#dataYAvailableColIds.includes(td.colIdAt(i))) {
            dataYFeatureMap.set(td.colIdAt(i), td.colTitleAt(i));
         }
      }

      if (JSON.stringify([...this.#dataYFeatureMap]) !== JSON.stringify([...dataYFeatureMap])) {
         this.#dataYFeatureMap = dataYFeatureMap;
         this.dataOptionValuesChanged.emit(this);
      }

      return [ this.#dataXColId, this.#dataYColId ];
   }

   updateData() {
      const td = this.tableData;
      if (this.#dataYColId) {
         const colWells = td.colData(this.wellColId);
         const colXMeta = this.#dataXColId ? td.colMetadata(this.#dataXColId) : null;
         const colYMeta = td.colMetadata(this.#dataYColId);
         const colXData = this.#dataXColId ? td.colData(this.#dataXColId) : null;
         const colYData = td.colData(this.#dataYColId);

         const dataPerWell = new Map();
         for (let i = 0; i < colWells.length; i++) {
            const wellName = colWells[i];
            if (Number.isFinite(colYData[i]) && dataPerWell.has(wellName)) {
               dataPerWell.get(wellName).push([colXData?.[i],colYData[i]]);
            }
            else if (Number.isFinite(colYData[i])) {
               dataPerWell.set(wellName, [[colXData?.[i],colYData[i]]]);
            }
         }

         const minX = colXMeta?.globalRange?.min ?? (colXData ? Math.min(...colXData) : null);
         const maxX = colXMeta?.globalRange?.max ?? (colXData ? Math.max(...colXData) : null);
         const minY = colYMeta?.globalRange?.min ?? Math.min(...colYData);
         const maxY = colYMeta?.globalRange?.max ?? Math.max(...colYData);

         if (this.legend) {
            while (this.legend.children.length)
               this.legend.removeChild(this.legend.children[0]);

            let item = document.createElement('span')
            item.className = 'lim-item';
            let glyph = document.createElement('span');
            glyph.className = 'lim-glyph';
            glyph.innerHTML = `<svg width="16" height="9"><line x1="0" y1="3" x2="16" y2="3" class="linechart" /></svg>`;
            item.appendChild(glyph);
            let label = document.createElement('span');
            label.className = 'lim-label';
            label.innerText = this.#dataXColId ? `${td.colTitleAndUnit(this.#dataYColId)} vs ${td.colTitleAndUnit(this.#dataXColId)}` : td.colTitleAndUnit(this.#dataYColId);
            item.appendChild(label);
            this.legend.appendChild(item);
         }

         const size = this.wellContentSize;
         this.populateTable((cell, wellName) => {
            const wellData = dataPerWell.get(wellName);
            if (!wellData || !Array.isArray(wellData) || !wellData.length)
               return;

            let points = [];
            if (colXData)
               wellData.sort((a, b) => (a[0] < b[0] ? -1 : (a[0] == b[0] ? 0 : 1)));
            let startLine = true;
            for (let i = 0; i < wellData.length; i++) {
               const xy = wellData[i];
               const x = Number.isFinite(xy[0]) ? 2 + (size-4) * (colXData ? (xy[0] - minX) / (maxX - minX) : i / (wellData.length - 1)) : null;
               const y = Number.isFinite(xy[1]) ? 2 + (size-4) * (xy[1] - minY) / (maxY - minY) : null;
               if(x && y)
                  points.push(startLine ? `M${x} ${y}` : `L${x} ${y}`);
               startLine = !x || !y;
            }

            /*cell.innerHTML = `<svg width="${size}" height="${size}">
               <path d="M${points[0].substring(1)} ${points.slice(1).join(' ')}" transform="scale(1, -1) translate(0, ${-size})" class="linechart" />
            </svg>`;*/
            cell.innerHTML = `
            <div style="top:${-size/2}px;width:${size};height:${size};position:relative;">
               <svg viewbox="0 0 ${size} ${size}" style="top:0;left:0;width:${size};height:${size};position:absolute;">
                  <path d="M${points[0].substring(1)} ${points.slice(1).join(' ')}" transform="scale(1, -1) translate(0, ${-size})" class="linechart" />
               </svg>
            </div>`;
            cell.className = 'selenabled';
         });
      }
      else {
         this.populateTable((cell, wellName) => { });
         if (!this.#dataXColId) {
            this.errorMessage = "No x-axis clumn defined!";
         } else if (!this.#dataYColId) {
            this.errorMessage = "No y-axis clumn defined!";
         }
      }
      return true;
   }

   get dataYColId() {
      return this.#dataYColId;
   }

   set dataYColId(val) {
      if (this.#dataYColId === val)
         return;
      this.#dataYColId = val;
      this.dataOptionValueChanged.emit(this);
      this.recreate();
   }
}


customElements.define('lim-wellplate-view-bars', LimPlateViewBars);
customElements.define('lim-wellplate-view-barstack', LimPlateViewBarStack);
customElements.define('lim-wellplate-view-boxplot', LimPlateViewBoxplot);
customElements.define('lim-wellplate-view-dosing', LimPlateViewDosing);
customElements.define('lim-wellplate-view-heatmap', LimPlateViewHeatmap);
customElements.define('lim-wellplate-view-image', LimPlateViewImage);
customElements.define('lim-wellplate-view-labeling', LimPlateViewLabeling);
customElements.define('lim-wellplate-view-violin', LimPlateViewViolin);
customElements.define('lim-wellplate-view-linechart', LimPlateViewLineChart);

const createPlateView = (plateViewClass, ...pars) => {
   const el = new plateViewClass();
   el.initialize(...pars);
   return el;
}

LimClassFactory.registerConstructor("LimPlateViewBars", (...pars) => createPlateView(LimPlateViewBars, ...pars));
LimClassFactory.registerConstructor("LimPlateViewBarStack", (...pars) => createPlateView(LimPlateViewBarStack, ...pars));
LimClassFactory.registerConstructor("LimPlateViewBoxplot", (...pars) => createPlateView(LimPlateViewBoxplot, ...pars));
LimClassFactory.registerConstructor("LimPlateViewDosing", (...pars) => createPlateView(LimPlateViewDosing, ...pars));
LimClassFactory.registerConstructor("LimPlateViewHeatmap", (...pars) => createPlateView(LimPlateViewHeatmap, ...pars));
LimClassFactory.registerConstructor("LimPlateViewImage", (...pars) => createPlateView(LimPlateViewImage, ...pars));
LimClassFactory.registerConstructor("LimPlateViewLabeling", (...pars) => createPlateView(LimPlateViewLabeling, ...pars));
LimClassFactory.registerConstructor("LimPlateViewViolin", (...pars) => createPlateView(LimPlateViewViolin, ...pars));
LimClassFactory.registerConstructor("LimPlateViewLineChart", (...pars) => createPlateView(LimPlateViewLineChart, ...pars));



