mirror of
				https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
				synced 2025-11-04 04:12:45 +00:00 
			
		
		
		
	Outsource Static Viz to extensions logic
This commit is contained in:
		@@ -1,11 +1,6 @@
 | 
			
		||||
class CorpusAnalysisApp {
 | 
			
		||||
  constructor(corpusId) {
 | 
			
		||||
    this.data = {
 | 
			
		||||
      stopwords: undefined,
 | 
			
		||||
      originalStopwords: {},
 | 
			
		||||
      stopwordCache: {},
 | 
			
		||||
      promises: {getStopwords: undefined}
 | 
			
		||||
    };
 | 
			
		||||
    this.data = {};
 | 
			
		||||
 | 
			
		||||
    // HTML elements
 | 
			
		||||
    this.elements = {
 | 
			
		||||
@@ -27,24 +22,6 @@ class CorpusAnalysisApp {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStopwords() {
 | 
			
		||||
    this.data.promises.getStopwords = new Promise((resolve, reject) => {
 | 
			
		||||
      Requests.corpora.entity.getStopwords()
 | 
			
		||||
        .then((response) => {
 | 
			
		||||
          response.json()
 | 
			
		||||
            .then((json) => {
 | 
			
		||||
              this.data.originalStopwords = structuredClone(json);
 | 
			
		||||
              this.data.stopwords = structuredClone(json);
 | 
			
		||||
              resolve(this.data.stopwords);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
              reject(error);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    return this.data.promises.getStopwords;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init() {
 | 
			
		||||
    this.disableActionElements();
 | 
			
		||||
    this.elements.m.initModal.open();
 | 
			
		||||
@@ -57,11 +34,12 @@ class CorpusAnalysisApp {
 | 
			
		||||
      .then((cqiCorpora) => {
 | 
			
		||||
          this.data.corpus = {o: cqiCorpora[0]};
 | 
			
		||||
          console.log(this.data.corpus.o.staticData);
 | 
			
		||||
          this.renderGeneralCorpusInfo();
 | 
			
		||||
          this.renderTextInfoList();
 | 
			
		||||
          this.renderTextProportionsGraphic()
 | 
			
		||||
          this.renderFrequenciesGraphic();
 | 
			
		||||
          this.renderBoundsGraphic();
 | 
			
		||||
          // this.renderGeneralCorpusInfo();
 | 
			
		||||
          // this.renderTextInfoList();
 | 
			
		||||
          // this.renderTextProportionsGraphic()
 | 
			
		||||
          // this.renderFrequenciesGraphic();
 | 
			
		||||
          // this.renderBoundsGraphic();
 | 
			
		||||
          
 | 
			
		||||
          // TODO: Don't do this hgere
 | 
			
		||||
          this.data.corpus.o.updateDb();
 | 
			
		||||
          this.enableActionElements();
 | 
			
		||||
@@ -78,7 +56,6 @@ class CorpusAnalysisApp {
 | 
			
		||||
          progressElement.classList.add('hide');
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
    // Add event listeners
 | 
			
		||||
    for (let extensionSelectorElement of this.elements.overview.querySelectorAll('.extension-selector')) {
 | 
			
		||||
@@ -86,25 +63,6 @@ class CorpusAnalysisApp {
 | 
			
		||||
        this.elements.m.extensionTabs.select(extensionSelectorElement.dataset.target);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let frequenciesStopwordSettingModal = document.querySelector('#frequencies-stopwords-setting-modal');
 | 
			
		||||
    let frequenciesStopwordSettingModalButton = document.querySelector('#frequencies-stopwords-setting-modal-button');
 | 
			
		||||
    frequenciesStopwordSettingModalButton.addEventListener('click', () => {
 | 
			
		||||
      this.data.stopwordCache = structuredClone(this.data.stopwords);
 | 
			
		||||
      this.renderStopwordSettingsModal(this.data.stopwords);
 | 
			
		||||
      M.Modal.init(frequenciesStopwordSettingModal, {dismissible: false});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (let actionButton of document.querySelectorAll('.frequencies-stopword-setting-modal-action-buttons')) {
 | 
			
		||||
      actionButton.addEventListener('click', (event) => {
 | 
			
		||||
        let action = event.target.closest('.frequencies-stopword-setting-modal-action-buttons').dataset.action;
 | 
			
		||||
        if (action === 'submit') {
 | 
			
		||||
          this.renderFrequenciesGraphic();
 | 
			
		||||
        } else if (action === 'cancel') {
 | 
			
		||||
          this.data.stopwords = structuredClone(this.data.stopwordCache);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  registerExtension(extension) {
 | 
			
		||||
@@ -141,306 +99,4 @@ class CorpusAnalysisApp {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderGeneralCorpusInfo() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    document.querySelector('.corpus-num-tokens').innerHTML = corpusData.corpus.counts.token;
 | 
			
		||||
    document.querySelector('.corpus-num-s').innerHTML = corpusData.corpus.counts.s;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-words').innerHTML = Object.entries(corpusData.corpus.freqs.word).length;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-lemmas').innerHTML = Object.entries(corpusData.corpus.freqs.lemma).length;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-pos').innerHTML = Object.entries(corpusData.corpus.freqs.pos).length;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-simple-pos').innerHTML = Object.entries(corpusData.corpus.freqs.simple_pos).length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderTextInfoList() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let corpusTextInfoListElement = document.querySelector('.corpus-text-info-list');
 | 
			
		||||
    let corpusTextInfoList = new CorpusTextInfoList(corpusTextInfoListElement);
 | 
			
		||||
    let texts = corpusData.s_attrs.text.lexicon;
 | 
			
		||||
    let textData = [];
 | 
			
		||||
    for (let i = 0; i < Object.entries(texts).length; i++) {
 | 
			
		||||
      let resource = {
 | 
			
		||||
        title: corpusData.values.s_attrs.text[i].title,
 | 
			
		||||
        publishing_year: corpusData.values.s_attrs.text[i].publishing_year,
 | 
			
		||||
        num_tokens: corpusData.s_attrs.text.lexicon[i].counts.token,
 | 
			
		||||
        num_sentences: corpusData.s_attrs.text.lexicon[i].counts.s,
 | 
			
		||||
        num_unique_words: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.word).length,
 | 
			
		||||
        num_unique_lemmas: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.lemma).length,
 | 
			
		||||
        num_unique_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.pos).length,
 | 
			
		||||
        num_unique_simple_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.simple_pos).length
 | 
			
		||||
      };
 | 
			
		||||
  
 | 
			
		||||
      textData.push(resource);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    corpusTextInfoList.add(textData);
 | 
			
		||||
 | 
			
		||||
    let textCountChipElement = document.querySelector('.text-count-chip');
 | 
			
		||||
    textCountChipElement.innerHTML = `Text count: ${corpusData.corpus.counts.text}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderTextProportionsGraphic() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let textProportionsGraphicElement = document.querySelector('#text-proportions-graphic');
 | 
			
		||||
    let texts = Object.entries(corpusData.s_attrs.text.lexicon);
 | 
			
		||||
    let graphData = [
 | 
			
		||||
      {
 | 
			
		||||
        values: texts.map(text => text[1].counts.token),
 | 
			
		||||
        labels: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
        type: 'pie'
 | 
			
		||||
      }
 | 
			
		||||
    ];
 | 
			
		||||
    let graphLayout = {
 | 
			
		||||
      showlegend: true,
 | 
			
		||||
      height: 486,
 | 
			
		||||
      margin: {
 | 
			
		||||
        l: 10,
 | 
			
		||||
        r: 10,
 | 
			
		||||
        b: 10,
 | 
			
		||||
        t: 10
 | 
			
		||||
      },
 | 
			
		||||
      legend: {
 | 
			
		||||
        "orientation": "h",
 | 
			
		||||
        font: {
 | 
			
		||||
          size: 10
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    let config = {
 | 
			
		||||
      responsive: true,
 | 
			
		||||
      displaylogo: false
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Plotly.newPlot(textProportionsGraphicElement, graphData, graphLayout, config);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async renderFrequenciesGraphic() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let frequenciesTokenCategoryDropdownElement = document.querySelector('[data-target="frequencies-token-category-dropdown"]');
 | 
			
		||||
    let frequenciesTokenCategoryDropdownListElement = document.querySelector("#frequencies-token-category-dropdown");
 | 
			
		||||
    let frequenciesGraphicElement = document.querySelector('#frequencies-graphic');
 | 
			
		||||
    let texts = Object.entries(corpusData.s_attrs.text.lexicon);
 | 
			
		||||
    let graphtype = document.querySelector('.frequencies-graph-mode-button.disabled').dataset.graphType;
 | 
			
		||||
    let graphModeButtons = document.querySelectorAll('.frequencies-graph-mode-button');
 | 
			
		||||
 | 
			
		||||
    frequenciesTokenCategoryDropdownListElement.addEventListener('click', (event) => {
 | 
			
		||||
      frequenciesTokenCategoryDropdownElement.firstChild.textContent = event.target.innerHTML;
 | 
			
		||||
      this.renderFrequenciesGraphic(corpusData);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    graphModeButtons.forEach(graphModeButton => {
 | 
			
		||||
      graphModeButton.addEventListener('click', (event) => {
 | 
			
		||||
        graphModeButtons.forEach(btn => {
 | 
			
		||||
          btn.classList.remove('disabled');
 | 
			
		||||
        });
 | 
			
		||||
        event.target.closest('.frequencies-graph-mode-button').classList.add('disabled');
 | 
			
		||||
        this.renderFrequenciesGraphic(corpusData);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
 | 
			
		||||
    
 | 
			
		||||
    let graphData = await this.createFrequenciesGraphData(tokenCategory, texts, corpusData, graphtype);
 | 
			
		||||
    let graphLayout = {
 | 
			
		||||
      barmode: graphtype === 'bar' ? 'stack' : '',
 | 
			
		||||
      margin: {
 | 
			
		||||
        t: 20,
 | 
			
		||||
        l: 50
 | 
			
		||||
      },
 | 
			
		||||
      yaxis: {
 | 
			
		||||
        showticklabels: graphtype === 'markers' ? false : true
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    let config = {
 | 
			
		||||
      responsive: true,
 | 
			
		||||
      modeBarButtonsToRemove: ['zoom2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
 | 
			
		||||
      displaylogo: false
 | 
			
		||||
    };
 | 
			
		||||
    Plotly.newPlot(frequenciesGraphicElement, graphData, graphLayout, config);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createFrequenciesGraphData(category, texts, corpusData, graphtype) {
 | 
			
		||||
    let stopwords = this.data.stopwords;
 | 
			
		||||
    if (this.data.stopwords === undefined) {
 | 
			
		||||
      stopwords = await this.getStopwords();
 | 
			
		||||
    }
 | 
			
		||||
    let stopwordList = Object.values(stopwords).flat();
 | 
			
		||||
    let graphData = [];
 | 
			
		||||
    let filteredData = Object.entries(corpusData.corpus.freqs[category])
 | 
			
		||||
      .sort((a, b) => b[1] - a[1])
 | 
			
		||||
      .filter(item => !stopwordList.includes(corpusData.values.p_attrs[category][item[0]].toLowerCase()))
 | 
			
		||||
      .slice(0, 5);
 | 
			
		||||
 | 
			
		||||
    if (graphtype !== 'markers') {
 | 
			
		||||
      for (let item of filteredData) {
 | 
			
		||||
        let data = {
 | 
			
		||||
          x: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
          y: texts.map(text => text[1].freqs[category][item[0]] || 0),
 | 
			
		||||
          name: corpusData.values.p_attrs[category][item[0]],
 | 
			
		||||
          type: graphtype
 | 
			
		||||
        };
 | 
			
		||||
        graphData.push(data);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      for (let item of filteredData) {
 | 
			
		||||
        let size = texts.map(text => text[1].freqs[category][item[0]] || 0);
 | 
			
		||||
        let data = {
 | 
			
		||||
          x: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
          y: texts.map(text => corpusData.values.p_attrs[category][item[0]]),
 | 
			
		||||
          name: corpusData.values.p_attrs[category][item[0]],
 | 
			
		||||
          text: texts.map(text => `${corpusData.values.p_attrs[category][item[0]]}<br>${text[1].freqs[category][item[0]] || 0}`),
 | 
			
		||||
          mode: 'markers',
 | 
			
		||||
          marker: {
 | 
			
		||||
            size: size,
 | 
			
		||||
            sizeref: 0.4
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
        graphData.push(data);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return graphData;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderStopwordSettingsModal(stopwords) {
 | 
			
		||||
    let stopwordInputField = document.querySelector('#stopword-input-field');
 | 
			
		||||
    let userStopwordListContainer = document.querySelector('#user-stopword-list-container');
 | 
			
		||||
    let stopwordLanguageSelection = document.querySelector('#stopword-language-selection');
 | 
			
		||||
    let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
 | 
			
		||||
    let deleteLanguageStopwordListEntriesButton = document.querySelector('#delete-language-stopword-list-entries-button');
 | 
			
		||||
    let resetLanguageStopwordListEntriesButton = document.querySelector('#reset-language-stopword-list-entries-button');
 | 
			
		||||
 | 
			
		||||
    stopwordLanguageChipList.innerHTML = '';
 | 
			
		||||
    userStopwordListContainer.innerHTML = '';
 | 
			
		||||
    stopwordInputField.value = '';
 | 
			
		||||
    
 | 
			
		||||
    // Render stopword language selection. Set english as default language. Filter out user_stopwords.
 | 
			
		||||
    if (stopwordLanguageSelection.children.length === 0) {
 | 
			
		||||
      Object.keys(stopwords).forEach(language => {
 | 
			
		||||
        if (language !== 'user_stopwords') {
 | 
			
		||||
          let optionElement = Utils.HTMLToElement(`<option value="${language}" ${language === 'english' ? 'selected' : ''}>${language}</option>`);
 | 
			
		||||
          stopwordLanguageSelection.appendChild(optionElement);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Render user stopwords over input field.
 | 
			
		||||
    if (this.data.stopwords['user_stopwords'].length > 0) {
 | 
			
		||||
      for (let word of this.data.stopwords['user_stopwords']) {
 | 
			
		||||
        let chipElement = Utils.HTMLToElement(`<div class="chip">${word}<i class="close material-icons">close</i></div>`);
 | 
			
		||||
        chipElement.addEventListener('click', (event) => {
 | 
			
		||||
          let removedListItem = event.target.closest('.chip').firstChild.textContent;
 | 
			
		||||
          this.data.stopwords['user_stopwords'] = structuredClone(this.data.stopwords['user_stopwords'].filter(item => item !== removedListItem));
 | 
			
		||||
        });
 | 
			
		||||
        userStopwordListContainer.appendChild(chipElement);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Render english stopwords as default ...
 | 
			
		||||
    let selectedLanguage = document.querySelector('#stopword-language-selection').value;
 | 
			
		||||
    this.renderStopwordLanguageChipList(selectedLanguage, stopwords[selectedLanguage]);
 | 
			
		||||
    
 | 
			
		||||
    // ... or render selected language stopwords.
 | 
			
		||||
    stopwordLanguageSelection.addEventListener('change', (event) => {
 | 
			
		||||
      this.renderStopwordLanguageChipList(event.target.value, stopwords[event.target.value]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Eventlistener for deleting all stopwords of a language.
 | 
			
		||||
    deleteLanguageStopwordListEntriesButton.addEventListener('click', (event) => {
 | 
			
		||||
      let selectedLanguage = stopwordLanguageSelection.value;
 | 
			
		||||
      this.data.stopwords[selectedLanguage] = [];
 | 
			
		||||
      stopwordLanguageChipList.innerHTML = '';
 | 
			
		||||
      this.buttonRendering();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Eventlistener for resetting all stopwords of a language to the original stopwords.
 | 
			
		||||
    resetLanguageStopwordListEntriesButton.addEventListener('click', () => {
 | 
			
		||||
      let selectedLanguage = stopwordLanguageSelection.value;
 | 
			
		||||
      this.data.stopwords[selectedLanguage] = structuredClone(this.data.originalStopwords[selectedLanguage]);
 | 
			
		||||
      this.renderStopwordLanguageChipList(selectedLanguage, this.data.stopwords[selectedLanguage]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Initialize Materialize components.
 | 
			
		||||
    M.Chips.init(
 | 
			
		||||
        stopwordInputField, 
 | 
			
		||||
        {
 | 
			
		||||
          placeholder: 'Add stopwords',
 | 
			
		||||
          onChipAdd: (event) => {
 | 
			
		||||
            for (let word of event[0].M_Chips.chipsData) {
 | 
			
		||||
              if (!this.data.stopwords['user_stopwords'].includes(word.tag.toLowerCase())) {
 | 
			
		||||
                this.data.stopwords['user_stopwords'].push(word.tag.toLowerCase());
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    M.FormSelect.init(stopwordLanguageSelection);
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buttonRendering() {
 | 
			
		||||
    let deleteLanguageStopwordListEntriesButton = document.querySelector('#delete-language-stopword-list-entries-button');
 | 
			
		||||
    let resetLanguageStopwordListEntriesButton = document.querySelector('#reset-language-stopword-list-entries-button');
 | 
			
		||||
    let selectedLanguage = document.querySelector('#stopword-language-selection').value;
 | 
			
		||||
    let stopwordLength = this.data.stopwords[selectedLanguage].length;
 | 
			
		||||
    let originalStopwordListLength = this.data.originalStopwords[selectedLanguage].length;
 | 
			
		||||
 | 
			
		||||
    deleteLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === 0);
 | 
			
		||||
    resetLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === originalStopwordListLength);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  renderStopwordLanguageChipList(language, stopwords) {
 | 
			
		||||
    let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
 | 
			
		||||
    stopwordLanguageChipList.innerHTML = '';
 | 
			
		||||
    for (let word of stopwords) {
 | 
			
		||||
      let chipElement = Utils.HTMLToElement(`<div class="chip">${word}<i class="close material-icons">close</i></div>`);
 | 
			
		||||
      chipElement.addEventListener('click', (event) => {
 | 
			
		||||
        let removedListItem = event.target.closest('.chip').firstChild.textContent;
 | 
			
		||||
        this.data.stopwords[language] = structuredClone(this.data.stopwords[language].filter(item => item !== removedListItem));
 | 
			
		||||
        this.buttonRendering();
 | 
			
		||||
      });
 | 
			
		||||
      stopwordLanguageChipList.appendChild(chipElement);
 | 
			
		||||
    }
 | 
			
		||||
    this.buttonRendering();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  renderBoundsGraphic() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let boundsGraphicElement = document.querySelector('#bounds-graphic');
 | 
			
		||||
 | 
			
		||||
    let graphData = [];
 | 
			
		||||
    let texts = Object.entries(corpusData.s_attrs.text.lexicon);
 | 
			
		||||
 | 
			
		||||
    graphData = [{
 | 
			
		||||
      type: 'bar',
 | 
			
		||||
      x: texts.map(text => text[1].bounds[1] - text[1].bounds[0]),
 | 
			
		||||
      y: texts.map(text => corpusData.values.s_attrs.text[text[0]].title),
 | 
			
		||||
      base: texts.map(text => text[1].bounds[0]),
 | 
			
		||||
      text: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
      orientation: 'h',
 | 
			
		||||
      hovertemplate: '%{base} - %{x} <br>%{y}',
 | 
			
		||||
      showlegend: false
 | 
			
		||||
    }];
 | 
			
		||||
 | 
			
		||||
    let graphLayout = {
 | 
			
		||||
      barmode: 'stack',
 | 
			
		||||
      type: 'bar',
 | 
			
		||||
      showgrid: false,
 | 
			
		||||
      xaxis: {
 | 
			
		||||
        rangemode: 'nonnegative',
 | 
			
		||||
        autorange: true
 | 
			
		||||
      },
 | 
			
		||||
      yaxis: {
 | 
			
		||||
        autorange: true,
 | 
			
		||||
        showticklabels: false
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let config = {
 | 
			
		||||
      responsive: true,
 | 
			
		||||
      modeBarButtonsToRemove: ['zoom2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
 | 
			
		||||
      displaylogo: false
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    Plotly.newPlot(boundsGraphicElement, graphData, graphLayout, config);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,364 @@
 | 
			
		||||
class CorpusAnalysisStaticVisualization {
 | 
			
		||||
  name = 'Static Visualization';
 | 
			
		||||
 | 
			
		||||
  constructor(app) {
 | 
			
		||||
    this.app = app;
 | 
			
		||||
    this.data = {
 | 
			
		||||
      stopwords: undefined,
 | 
			
		||||
      originalStopwords: {},
 | 
			
		||||
      stopwordCache: {},
 | 
			
		||||
      promises: {getStopwords: undefined}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.app.registerExtension(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init() {
 | 
			
		||||
    // Init data
 | 
			
		||||
    this.data.corpus = this.app.data.corpus;
 | 
			
		||||
    this.renderGeneralCorpusInfo();
 | 
			
		||||
    this.renderTextInfoList();
 | 
			
		||||
    this.renderTextProportionsGraphic()
 | 
			
		||||
    this.renderFrequenciesGraphic();
 | 
			
		||||
    this.renderBoundsGraphic();
 | 
			
		||||
    // Add event listeners
 | 
			
		||||
    let frequenciesStopwordSettingModal = document.querySelector('#frequencies-stopwords-setting-modal');
 | 
			
		||||
    let frequenciesStopwordSettingModalButton = document.querySelector('#frequencies-stopwords-setting-modal-button');
 | 
			
		||||
    frequenciesStopwordSettingModalButton.addEventListener('click', () => {
 | 
			
		||||
      this.data.stopwordCache = structuredClone(this.data.stopwords);
 | 
			
		||||
      this.renderStopwordSettingsModal(this.data.stopwords);
 | 
			
		||||
      M.Modal.init(frequenciesStopwordSettingModal, {dismissible: false});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    for (let actionButton of document.querySelectorAll('.frequencies-stopword-setting-modal-action-buttons')) {
 | 
			
		||||
      actionButton.addEventListener('click', (event) => {
 | 
			
		||||
        let action = event.target.closest('.frequencies-stopword-setting-modal-action-buttons').dataset.action;
 | 
			
		||||
        if (action === 'submit') {
 | 
			
		||||
          this.renderFrequenciesGraphic();
 | 
			
		||||
        } else if (action === 'cancel') {
 | 
			
		||||
          this.data.stopwords = structuredClone(this.data.stopwordCache);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStopwords() {
 | 
			
		||||
    this.data.promises.getStopwords = new Promise((resolve, reject) => {
 | 
			
		||||
      Requests.corpora.entity.getStopwords()
 | 
			
		||||
        .then((response) => {
 | 
			
		||||
          response.json()
 | 
			
		||||
            .then((json) => {
 | 
			
		||||
              this.data.originalStopwords = structuredClone(json);
 | 
			
		||||
              this.data.stopwords = structuredClone(json);
 | 
			
		||||
              resolve(this.data.stopwords);
 | 
			
		||||
            })
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
              reject(error);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    return this.data.promises.getStopwords;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderGeneralCorpusInfo() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    document.querySelector('.corpus-num-tokens').innerHTML = corpusData.corpus.counts.token;
 | 
			
		||||
    document.querySelector('.corpus-num-s').innerHTML = corpusData.corpus.counts.s;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-words').innerHTML = Object.entries(corpusData.corpus.freqs.word).length;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-lemmas').innerHTML = Object.entries(corpusData.corpus.freqs.lemma).length;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-pos').innerHTML = Object.entries(corpusData.corpus.freqs.pos).length;
 | 
			
		||||
    document.querySelector('.corpus-num-unique-simple-pos').innerHTML = Object.entries(corpusData.corpus.freqs.simple_pos).length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderTextInfoList() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let corpusTextInfoListElement = document.querySelector('.corpus-text-info-list');
 | 
			
		||||
    let corpusTextInfoList = new CorpusTextInfoList(corpusTextInfoListElement);
 | 
			
		||||
    let texts = corpusData.s_attrs.text.lexicon;
 | 
			
		||||
    let textData = [];
 | 
			
		||||
    for (let i = 0; i < Object.entries(texts).length; i++) {
 | 
			
		||||
      let resource = {
 | 
			
		||||
        title: corpusData.values.s_attrs.text[i].title,
 | 
			
		||||
        publishing_year: corpusData.values.s_attrs.text[i].publishing_year,
 | 
			
		||||
        num_tokens: corpusData.s_attrs.text.lexicon[i].counts.token,
 | 
			
		||||
        num_sentences: corpusData.s_attrs.text.lexicon[i].counts.s,
 | 
			
		||||
        num_unique_words: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.word).length,
 | 
			
		||||
        num_unique_lemmas: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.lemma).length,
 | 
			
		||||
        num_unique_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.pos).length,
 | 
			
		||||
        num_unique_simple_pos: Object.entries(corpusData.s_attrs.text.lexicon[i].freqs.simple_pos).length
 | 
			
		||||
      };
 | 
			
		||||
  
 | 
			
		||||
      textData.push(resource);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    corpusTextInfoList.add(textData);
 | 
			
		||||
 | 
			
		||||
    let textCountChipElement = document.querySelector('.text-count-chip');
 | 
			
		||||
    textCountChipElement.innerHTML = `Text count: ${corpusData.corpus.counts.text}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderTextProportionsGraphic() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let textProportionsGraphicElement = document.querySelector('#text-proportions-graphic');
 | 
			
		||||
    let texts = Object.entries(corpusData.s_attrs.text.lexicon);
 | 
			
		||||
    let graphData = [
 | 
			
		||||
      {
 | 
			
		||||
        values: texts.map(text => text[1].counts.token),
 | 
			
		||||
        labels: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
        type: 'pie'
 | 
			
		||||
      }
 | 
			
		||||
    ];
 | 
			
		||||
    let graphLayout = {
 | 
			
		||||
      showlegend: true,
 | 
			
		||||
      height: 486,
 | 
			
		||||
      margin: {
 | 
			
		||||
        l: 10,
 | 
			
		||||
        r: 10,
 | 
			
		||||
        b: 10,
 | 
			
		||||
        t: 10
 | 
			
		||||
      },
 | 
			
		||||
      legend: {
 | 
			
		||||
        "orientation": "h",
 | 
			
		||||
        font: {
 | 
			
		||||
          size: 10
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    let config = {
 | 
			
		||||
      responsive: true,
 | 
			
		||||
      displaylogo: false
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    Plotly.newPlot(textProportionsGraphicElement, graphData, graphLayout, config);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async renderFrequenciesGraphic() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let frequenciesTokenCategoryDropdownElement = document.querySelector('[data-target="frequencies-token-category-dropdown"]');
 | 
			
		||||
    let frequenciesTokenCategoryDropdownListElement = document.querySelector("#frequencies-token-category-dropdown");
 | 
			
		||||
    let frequenciesGraphicElement = document.querySelector('#frequencies-graphic');
 | 
			
		||||
    let texts = Object.entries(corpusData.s_attrs.text.lexicon);
 | 
			
		||||
    let graphtype = document.querySelector('.frequencies-graph-mode-button.disabled').dataset.graphType;
 | 
			
		||||
    let graphModeButtons = document.querySelectorAll('.frequencies-graph-mode-button');
 | 
			
		||||
 | 
			
		||||
    frequenciesTokenCategoryDropdownListElement.addEventListener('click', (event) => {
 | 
			
		||||
      frequenciesTokenCategoryDropdownElement.firstChild.textContent = event.target.innerHTML;
 | 
			
		||||
      this.renderFrequenciesGraphic(corpusData);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    graphModeButtons.forEach(graphModeButton => {
 | 
			
		||||
      graphModeButton.addEventListener('click', (event) => {
 | 
			
		||||
        graphModeButtons.forEach(btn => {
 | 
			
		||||
          btn.classList.remove('disabled');
 | 
			
		||||
        });
 | 
			
		||||
        event.target.closest('.frequencies-graph-mode-button').classList.add('disabled');
 | 
			
		||||
        this.renderFrequenciesGraphic(corpusData);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    let tokenCategory = frequenciesTokenCategoryDropdownElement.firstChild.textContent.toLowerCase();
 | 
			
		||||
    
 | 
			
		||||
    let graphData = await this.createFrequenciesGraphData(tokenCategory, texts, corpusData, graphtype);
 | 
			
		||||
    let graphLayout = {
 | 
			
		||||
      barmode: graphtype === 'bar' ? 'stack' : '',
 | 
			
		||||
      margin: {
 | 
			
		||||
        t: 20,
 | 
			
		||||
        l: 50
 | 
			
		||||
      },
 | 
			
		||||
      yaxis: {
 | 
			
		||||
        showticklabels: graphtype === 'markers' ? false : true
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    let config = {
 | 
			
		||||
      responsive: true,
 | 
			
		||||
      modeBarButtonsToRemove: ['zoom2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
 | 
			
		||||
      displaylogo: false
 | 
			
		||||
    };
 | 
			
		||||
    Plotly.newPlot(frequenciesGraphicElement, graphData, graphLayout, config);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async createFrequenciesGraphData(category, texts, corpusData, graphtype) {
 | 
			
		||||
    let stopwords = this.data.stopwords;
 | 
			
		||||
    if (this.data.stopwords === undefined) {
 | 
			
		||||
      stopwords = await this.getStopwords();
 | 
			
		||||
    }
 | 
			
		||||
    let stopwordList = Object.values(stopwords).flat();
 | 
			
		||||
    let graphData = [];
 | 
			
		||||
    let filteredData = Object.entries(corpusData.corpus.freqs[category])
 | 
			
		||||
      .sort((a, b) => b[1] - a[1])
 | 
			
		||||
      .filter(item => !stopwordList.includes(corpusData.values.p_attrs[category][item[0]].toLowerCase()))
 | 
			
		||||
      .slice(0, 5);
 | 
			
		||||
 | 
			
		||||
    if (graphtype !== 'markers') {
 | 
			
		||||
      for (let item of filteredData) {
 | 
			
		||||
        let data = {
 | 
			
		||||
          x: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
          y: texts.map(text => text[1].freqs[category][item[0]] || 0),
 | 
			
		||||
          name: corpusData.values.p_attrs[category][item[0]],
 | 
			
		||||
          type: graphtype
 | 
			
		||||
        };
 | 
			
		||||
        graphData.push(data);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      for (let item of filteredData) {
 | 
			
		||||
        let size = texts.map(text => text[1].freqs[category][item[0]] || 0);
 | 
			
		||||
        let data = {
 | 
			
		||||
          x: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
          y: texts.map(text => corpusData.values.p_attrs[category][item[0]]),
 | 
			
		||||
          name: corpusData.values.p_attrs[category][item[0]],
 | 
			
		||||
          text: texts.map(text => `${corpusData.values.p_attrs[category][item[0]]}<br>${text[1].freqs[category][item[0]] || 0}`),
 | 
			
		||||
          mode: 'markers',
 | 
			
		||||
          marker: {
 | 
			
		||||
            size: size,
 | 
			
		||||
            sizeref: 0.4
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
        graphData.push(data);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return graphData;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderStopwordSettingsModal(stopwords) {
 | 
			
		||||
    let stopwordInputField = document.querySelector('#stopword-input-field');
 | 
			
		||||
    let userStopwordListContainer = document.querySelector('#user-stopword-list-container');
 | 
			
		||||
    let stopwordLanguageSelection = document.querySelector('#stopword-language-selection');
 | 
			
		||||
    let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
 | 
			
		||||
    let deleteLanguageStopwordListEntriesButton = document.querySelector('#delete-language-stopword-list-entries-button');
 | 
			
		||||
    let resetLanguageStopwordListEntriesButton = document.querySelector('#reset-language-stopword-list-entries-button');
 | 
			
		||||
 | 
			
		||||
    stopwordLanguageChipList.innerHTML = '';
 | 
			
		||||
    userStopwordListContainer.innerHTML = '';
 | 
			
		||||
    stopwordInputField.value = '';
 | 
			
		||||
    
 | 
			
		||||
    // Render stopword language selection. Set english as default language. Filter out user_stopwords.
 | 
			
		||||
    if (stopwordLanguageSelection.children.length === 0) {
 | 
			
		||||
      Object.keys(stopwords).forEach(language => {
 | 
			
		||||
        if (language !== 'user_stopwords') {
 | 
			
		||||
          let optionElement = Utils.HTMLToElement(`<option value="${language}" ${language === 'english' ? 'selected' : ''}>${language}</option>`);
 | 
			
		||||
          stopwordLanguageSelection.appendChild(optionElement);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Render user stopwords over input field.
 | 
			
		||||
    if (this.data.stopwords['user_stopwords'].length > 0) {
 | 
			
		||||
      for (let word of this.data.stopwords['user_stopwords']) {
 | 
			
		||||
        let chipElement = Utils.HTMLToElement(`<div class="chip">${word}<i class="close material-icons">close</i></div>`);
 | 
			
		||||
        chipElement.addEventListener('click', (event) => {
 | 
			
		||||
          let removedListItem = event.target.closest('.chip').firstChild.textContent;
 | 
			
		||||
          this.data.stopwords['user_stopwords'] = structuredClone(this.data.stopwords['user_stopwords'].filter(item => item !== removedListItem));
 | 
			
		||||
        });
 | 
			
		||||
        userStopwordListContainer.appendChild(chipElement);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Render english stopwords as default ...
 | 
			
		||||
    let selectedLanguage = document.querySelector('#stopword-language-selection').value;
 | 
			
		||||
    this.renderStopwordLanguageChipList(selectedLanguage, stopwords[selectedLanguage]);
 | 
			
		||||
    
 | 
			
		||||
    // ... or render selected language stopwords.
 | 
			
		||||
    stopwordLanguageSelection.addEventListener('change', (event) => {
 | 
			
		||||
      this.renderStopwordLanguageChipList(event.target.value, stopwords[event.target.value]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Eventlistener for deleting all stopwords of a language.
 | 
			
		||||
    deleteLanguageStopwordListEntriesButton.addEventListener('click', (event) => {
 | 
			
		||||
      let selectedLanguage = stopwordLanguageSelection.value;
 | 
			
		||||
      this.data.stopwords[selectedLanguage] = [];
 | 
			
		||||
      stopwordLanguageChipList.innerHTML = '';
 | 
			
		||||
      this.buttonRendering();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Eventlistener for resetting all stopwords of a language to the original stopwords.
 | 
			
		||||
    resetLanguageStopwordListEntriesButton.addEventListener('click', () => {
 | 
			
		||||
      let selectedLanguage = stopwordLanguageSelection.value;
 | 
			
		||||
      this.data.stopwords[selectedLanguage] = structuredClone(this.data.originalStopwords[selectedLanguage]);
 | 
			
		||||
      this.renderStopwordLanguageChipList(selectedLanguage, this.data.stopwords[selectedLanguage]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Initialize Materialize components.
 | 
			
		||||
    M.Chips.init(
 | 
			
		||||
        stopwordInputField, 
 | 
			
		||||
        {
 | 
			
		||||
          placeholder: 'Add stopwords',
 | 
			
		||||
          onChipAdd: (event) => {
 | 
			
		||||
            for (let word of event[0].M_Chips.chipsData) {
 | 
			
		||||
              if (!this.data.stopwords['user_stopwords'].includes(word.tag.toLowerCase())) {
 | 
			
		||||
                this.data.stopwords['user_stopwords'].push(word.tag.toLowerCase());
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
    M.FormSelect.init(stopwordLanguageSelection);
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buttonRendering() {
 | 
			
		||||
    let deleteLanguageStopwordListEntriesButton = document.querySelector('#delete-language-stopword-list-entries-button');
 | 
			
		||||
    let resetLanguageStopwordListEntriesButton = document.querySelector('#reset-language-stopword-list-entries-button');
 | 
			
		||||
    let selectedLanguage = document.querySelector('#stopword-language-selection').value;
 | 
			
		||||
    let stopwordLength = this.data.stopwords[selectedLanguage].length;
 | 
			
		||||
    let originalStopwordListLength = this.data.originalStopwords[selectedLanguage].length;
 | 
			
		||||
 | 
			
		||||
    deleteLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === 0);
 | 
			
		||||
    resetLanguageStopwordListEntriesButton.classList.toggle('disabled', stopwordLength === originalStopwordListLength);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  renderStopwordLanguageChipList(language, stopwords) {
 | 
			
		||||
    let stopwordLanguageChipList = document.querySelector('#stopword-language-chip-list');
 | 
			
		||||
    stopwordLanguageChipList.innerHTML = '';
 | 
			
		||||
    for (let word of stopwords) {
 | 
			
		||||
      let chipElement = Utils.HTMLToElement(`<div class="chip">${word}<i class="close material-icons">close</i></div>`);
 | 
			
		||||
      chipElement.addEventListener('click', (event) => {
 | 
			
		||||
        let removedListItem = event.target.closest('.chip').firstChild.textContent;
 | 
			
		||||
        this.data.stopwords[language] = structuredClone(this.data.stopwords[language].filter(item => item !== removedListItem));
 | 
			
		||||
        this.buttonRendering();
 | 
			
		||||
      });
 | 
			
		||||
      stopwordLanguageChipList.appendChild(chipElement);
 | 
			
		||||
    }
 | 
			
		||||
    this.buttonRendering();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  renderBoundsGraphic() {
 | 
			
		||||
    let corpusData = this.data.corpus.o.staticData;
 | 
			
		||||
    let boundsGraphicElement = document.querySelector('#bounds-graphic');
 | 
			
		||||
 | 
			
		||||
    let graphData = [];
 | 
			
		||||
    let texts = Object.entries(corpusData.s_attrs.text.lexicon);
 | 
			
		||||
 | 
			
		||||
    graphData = [{
 | 
			
		||||
      type: 'bar',
 | 
			
		||||
      x: texts.map(text => text[1].bounds[1] - text[1].bounds[0]),
 | 
			
		||||
      y: texts.map(text => corpusData.values.s_attrs.text[text[0]].title),
 | 
			
		||||
      base: texts.map(text => text[1].bounds[0]),
 | 
			
		||||
      text: texts.map(text => `${corpusData.values.s_attrs.text[text[0]].title} (${corpusData.values.s_attrs.text[text[0]].publishing_year})`),
 | 
			
		||||
      orientation: 'h',
 | 
			
		||||
      hovertemplate: '%{base} - %{x} <br>%{y}',
 | 
			
		||||
      showlegend: false
 | 
			
		||||
    }];
 | 
			
		||||
 | 
			
		||||
    let graphLayout = {
 | 
			
		||||
      barmode: 'stack',
 | 
			
		||||
      type: 'bar',
 | 
			
		||||
      showgrid: false,
 | 
			
		||||
      xaxis: {
 | 
			
		||||
        rangemode: 'nonnegative',
 | 
			
		||||
        autorange: true
 | 
			
		||||
      },
 | 
			
		||||
      yaxis: {
 | 
			
		||||
        autorange: true,
 | 
			
		||||
        showticklabels: false
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let config = {
 | 
			
		||||
      responsive: true,
 | 
			
		||||
      modeBarButtonsToRemove: ['zoom2d', 'select2d', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d'],
 | 
			
		||||
      displaylogo: false
 | 
			
		||||
    };
 | 
			
		||||
  
 | 
			
		||||
    Plotly.newPlot(boundsGraphicElement, graphData, graphLayout, config);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user