diff --git a/mqm_viewer/README.md b/mqm_viewer/README.md
index 1a9f72e154a..a580298915e 100644
--- a/mqm_viewer/README.md
+++ b/mqm_viewer/README.md
@@ -1,251 +1,10 @@
-# MQM Viewer
+# MQM Viewer has been renamed "Marot"
-This repository contains a web app that can be used to analyze
-[Multidimensional Quality Metrics (MQM)](http://www.qt21.eu/mqm-definition/definition-2015-06-16.html)
-data from a human evaluation of translation quality. The web app can also
-display metrics computed by automated evaluations, such as BLEURT.
-
-To use it, download the files `mqm-viewer.html`, `mqm-viewer.js`,
-`mqm-sigtests.js`, and `mqm-viewer.css` to your computer:
-
-```
-wget https://raw.githubusercontent.com/google-research/google-research/master/mqm_viewer/mqm-viewer.{html,js,css}
-```
-
-Then, simply open the `mqm-viewer.html` file in a web browser, and use
-the "Choose files" button to pick one or more MQM data files. MQM data spans
-several columns, so it's best to use a desktop or laptop computer with a wide
-screen.
-
-A simpler option may be to just download the `mqm-viewer-lite.html` file and
-open it in a web browser (it loads the needed JavaScript and CSS files from
-a Google-hosted server).
-
-This is not an officially supported Google product.
-
-## Data file format
-
-The data file should have tab-separated UTF-8-encoded data with the following
-ten columns, one line per marked error:
-
-- **system**: Name of the translation system.
-- **doc**: Name of the document. It's useful to suffix this with language-pair,
- (eg., "doc42:English-German"), especially as you may want to view the data
- from several evaluations together.
-- **docSegId**: Id of segment (sentence or group of sentences) within the
- document.
-- **globalSegId**: Id of segment across all documents. If you do not have
- such numbering available, set this to a constant value, say 0.
-- **rater**: Rater who evaluated segment. If this row only carries metadata
- such as automated metrics and/or references, then `rater` will be the empty
- string (as will be `category` and `severity`).
-- **source**: Source text for segment.
-- **target**: Translated text for segment.
-- **category**: MQM error category (or "no-error").
-- **severity**: MQM error severity (or "no-error").
-- **metadata**: JSON-formatted object that may contain the following fields,
- among others:
- - **timestamp**: Time at which this annotation was obtained (milliseconds
- since Unix epoch)
- - **note**: Free-form text note provided by the rater with some annotations
- (notably, with the "Other" error category)
- - **corrected_translation**: If the rater provided a corrected translation,
- for the segment, it will be included here.
- - **source_not_seen**: This will be set to true if this annotation was marked
- without the source text of the segment being visible.
- - **source_spans**: Array of pairs of 0-based indices (usually just one)
- identifying the indices of the first and last source tokens in the marked
- span. These indices refer to the source_tokens array in the segment
- object.
- - **target_spans**: Array of pairs of 0-based indices (usually just one)
- identifying the indices of the first and last target tokens in the marked
- span. These indices refer to the target_tokens array in the segment
- object.
- - **marked_text**: The text that has been marked by the rater (or the
- empty string if this metadata is not associated with an marked span). This
- field is computed from source_spans/target_spans. It can be useful
- when filtering.
- - **segment**: An object that has information about the segment (from the
- current doc+docSegId+system) that is not specific to any particular
- annotation/rater. This object may not necessarily be repeated across
- multiple ratings for the same segment. The segment object may contain the
- following fields:
- - **references**: A mapping from names of references to the references
- themselves (e.g., {"ref_A": "The reference", "ref_B": "..."}). This
- field need not be repeated across different systems.
- - **primary_reference**: The name of the primary reference, which is
- a key in the "references" mapping (e.g., "ref_A"). This field is
- required if "references" is present. This field too need not be repeated
- across different systems.
- - **metrics**: A dictionary in which the keys are the names of metrics
- (such as "Bleurt-X") and values are the numbers for those metrics. The
- metric name "MQM" is used for the MQM score. Note that this MQM score
- for the segment is computed *without any filtering*.
- - **source_tokens**: An array of source text tokens.
- - **target_tokens**: An array of target text tokens.
- - **source_sentence_tokens**: An array specifying sentence segmentation
- in the source segment. Each entry is the number of tokens in one
- sentence.
- - **target_sentence_tokens**: An array specifying sentence segmentation
- in the target segment. Each entry is the number of tokens in one
- sentence.
- - **starts_paragraph**: A boolean that is true if this segment is the
- start of a new paragraph.
- - In addition, any text annotation fields present in the input data are
- copied here. In [Anthea's data format](https://github.com/google-research/google-research/blob/master/anthea/anthea-help.html),
- this would be all the fields present in the optional last column.
- - **feedback**: An object optionally present in the metadata of the first
- segment of a doc. This captures any feedback the rater may have provided.
- It can include a free-form text field (keyed by **notes**) and a string
- keyed by **thumbs** that is set to either "up" or "down".
- - **evaluation**: An object that has information about the evaluation used.
- This field is typically only present in the very first data row, and is
- not repeated, in order to save space. This object may contain the following
- fields:
- - **template**: The name of the template used ("MQM", "MQM-WebPage",
- etc.).
- - **config**: The configuration parameters that define the template. This
- includes "errors" and "severities". Some bulky fields, notably
- "instructions" and "description" may have been stripped out from this
- object.
- - **source_language**, **target_language**: Language codes.
- In MQMViewer, each metadata.evaluation object found is logged in the
- JavaScript debug console.
-
-The "metadata" column used to be an optional "note" column, and MQM Viewer
-continues to support that legacy format. Going forward, the metadata object
-may be augmented to contain additional information about the rating/segment.
-
-An optional header line in the data file will be ignored (identified by the
-presence of the text "system\tdoc").
-
-Example data files and details on score computations can be found in this
-[GitHub repository](https://github.com/google/wmt-mqm-human-evaluation).
-
-## Data format conversion
-
-You can easily add format conversion code that can convert arbitrarily
-formatted data (for example, JSON lines from a BLEURT decoder), by adding a
-JavaScript function with the following name and behavior:
-
-```
-/**
- * Transform data (that may be in some custom format) into the MQM data format.
- * Pass through the data if no conversion was appropriate or necessary.
- * @param {string} sourceName The file name or URL source for the data.
- * @param {string} data The original data.
- * @return {string} The MQM-data-formatted data.
- */
-function mqmDataConvertor(sourceName, data) {
- ...
- return data;
-}
-```
-
-## Data from URLs
-
-You can pass a `?dataurls=,...` parameter to MQM Viewer, to load data
-from the URLs listed. Note that any URLs have to be hosted on the same site
-as the viewer itself, or need to have a CORS exception.
-
-If your domain uses some custom way of storing data (Google uses the CNS file
-system, for example) that uses a way to convert data names to URLs, and you wish
-to directly pass such data names as URLs (to `?dataurls=`), then you can add a
-JavaScript function with the following name and behavior:
-```
-/**
- * Transform a data name (that may be in some custom format) to a URL.
- * @param {string} dataName The name or identifier for the data.
- * @return {string} The URL from which the data can be loaded.
- */
-function mqmURLMaker(dataName) {
- /** Code to convert dataName into url */
- let url = ...;
- return url;
-}
-```
-
-## Filtering
-
-This web app facilitates interactive slicing and dicing of the data to identify
-interesting subsets, to compare translation systems along various dimensions,
-etc. The scores shown are always updated to reflect the currently active
-filters.
-
-- You can click on any System/Doc/ID/Rater/Category/Severity (or pick
- from the drop-down list under the column name) to set its **column
- filter** to that specific value.
-- You can provide **column filter** regular expressions for filtering
- one or more columns, in the input fields provided under the column names.
-- You can create sophisticated filters (involving multiple columns, for
- example) using a **JavaScript filter expression**.
- - This allows you to filter using any expression
- involving the columns. It can use the following
- variables: **system**, **doc**, **docSegId**,
- **globalSegId**, **rater**, **category**, **severity**,
- **source**, **target**, **metadata**.
- - Filter expressions also have access to three aggregated objects in
- variables named **aggrDoc**, **aggrDocSeg**, and **aggrDocSegSys**.
- The aggrDocSegSys dict also contains aggrDocSeg (with the key
- "aggrDocSeg"), which in turn similarly contains aggrDoc.
- - **aggrDoc** has the following properties:
- **doc**, **thumbsUpCount**, **thumbsDownCount**.
- - **aggrDocSeg** is an object with the following properties:
- - **aggrDocSeg.catsBySystem**,
- - **aggrDocSeg.catsByRater**,
- - **aggrDocSeg.sevsBySystem**,
- - **aggrDocSeg.sevsByRater**,
- - **aggrDocSeg.sevcatsBySystem**,
- - **aggrDocSeg.sevcatsByRater**,
- - **aggrDocSeg.source_tokens**,
- - **aggrDocSeg.source_sentence_tokens**,
- - **aggrDocSeg.starts_paragraph**,
- - **aggrDocSeg.references** (if available),
- - **aggrDocSeg.primary_reference** (if available),
- Each of these properties is an object keyed by system or rater, with the
- values being arrays of strings. The "sevcats\*" values look like
- "Minor/Fluency/Punctuation" or are just the same as severities if
- categories are empty. This segment-level aggregation allows you
- to select specific segments rather than just specific error ratings.
- - **aggrDocSeg.metrics** is an object keyed by the metric name and then by
- system name. It provides the segment's metric scores (including MQM) for
- all systems for which a metric is available for that segment.
- - **aggrDocSegSys** is just an alias for metadata.segment.
- - **Example**: docSegId > 10 || severity == 'Major'
- - **Example**: target.indexOf('thethe') >= 0
- - **Example**: metadata.marked_text.length >= 10
- - **Example**: aggrDocSeg.sevsBySystem['System-42'].includes('Major')
- - **Example**: aggrDocSegSys.metrics['MQM'] > 4 &&
- (aggrDocSegSys.metrics['BLEURT-X'] ?? 1) < 0.1.
- - **Example**: JSON.stringify(aggrDocSeg.sevcatsBySystem).includes('Major/Fl')
- - You can examine the metadata associated with any using the **Log metadata**
- interface shown in the **Filters** section. This can be useful for crafting
- filter expressions.
-
-## Significance tests
-When there are multiple systems that have been evaluated on common document
-segments, significance tests are run for each pair of systems and the resulting
-p-values are displayed in a table. The testing is done via paired one-sided
-approximate randomization (PAR), which corresponds to 'alternative="greater"'
-in [scipy's API](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.permutation_test.html).
-
-The significance tests are recomputed with any filtering that is applied. The
-computations are run in a background Worker thread. The tests include any
-available automated metrics in addition to MQM.
-
-## Data Notes
-There are some nuances to the data format which are useful to be aware of:
-
- - Marked spans are noted in the source/target text using `...` tags
- to enclose them. For example: `The error is here.`
- - Except in some legacy data, error spans are also identified at precise
- token-level using the `metadata.source_spans` and `metadata.target_spans`
- fields.
- - Severity and category names come directly from annotation tools and may
- have subtle variations (such as lowercase/uppercase differences or
- space-underscore changes).
- - Error spans may include leading/trailing whitespace if the annotation tool
- allows for this, which may or may not be part of the actual errors.
- For example, `The error is here.`
- The error spans themselves can also be entirely whitespace.
+The "MQM Viewer" tool has been renamed "Marot". Marot allows you to view not
+just [Multidimensional Quality Metrics
+(MQM)](http://www.qt21.eu/mqm-definition/definition-2015-06-16.html) human
+evaluations of translation quality, but also automated evaluations, such as
+BLEURT.
+[Please follow this link to find the Marot
+project.](https://github.com/google-research/google-research/tree/master/marot)
\ No newline at end of file
diff --git a/mqm_viewer/mqm-sigtests.js b/mqm_viewer/mqm-sigtests.js
deleted file mode 100644
index 193110ba504..00000000000
--- a/mqm_viewer/mqm-sigtests.js
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright 2023 The Google Research Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * This file contains the background Worker thread code that computes
- * significance tests for MQM score rankings of system pairs.
- *
- * The significance testing is done through paired one-sided approximate
- * randomization (PAR). The implemention follows sacrebleu at
- * https://github.com/mjpost/sacrebleu/blob/078c440168c6adc89ba75fe6d63f0d922d42bcfe/sacrebleu/significance.py#L112.
- */
-
-/**
- * Samples from [0, max) for a specified number of times.
- * @param {number} max
- * @param {number} size
- * @return {!Array}
- */
-function mqmGetRandomInt(max, size) {
- let samples = [];
- for (let i = 0; i < size; i++) {
- samples.push(Math.floor(Math.random() * max));
- }
- return samples;
-}
-
-/**
- * Performs one trial of paired approximate randomization for a given baseline
- * and a system and returns the score difference. Returns null if no common
- * segments are found.
- * @param {!MQMSigtestsData} data
- * @param {string} baseline
- * @param {string} system
- * @return {number}
- */
-function mqmPAROneTrial(data, baseline, system) {
- const baselineScores = data.segScoresBySystem[baseline];
- const systemScores = data.segScoresBySystem[system];
- const commonPos = data.commonPosBySystemPair[baseline][system];
-
- if (!commonPos) {
- return null;
- }
-
- /**
- * This random array indicates which shuffled system a given score should be
- * assigned to.
- */
- const permutations = mqmGetRandomInt(2, commonPos.length);
- let shufA = 0.0;
- let shufB = 0.0;
- for (let [idx, perm] of permutations.entries()) {
- const pos = commonPos[idx];
- if (perm == 0) {
- shufA += baselineScores[pos];
- shufB += systemScores[pos];
- } else {
- shufA += systemScores[pos];
- shufB += baselineScores[pos];
- }
- }
- shufA /= commonPos.length;
- shufB /= commonPos.length;
- return (shufA - shufB) * (data.lowerBetter ? 1.0 : -1.0);
-}
-
-/**
- * Implements the core logic to perform paired one-sided approximate
- * randomization by incrementally conducting trials.
- * @param {!Event} e is the message event received from the parent thread.
- * The e.data field is the mqmSigtestsData object that contains various
- * pieces of data needed.
- */
-function mqmPAR(e) {
- const mqmSigtestsData = e.data;
- const finishedUpdate = {
- finished: true,
- };
- for (let metric in mqmSigtestsData.metricData) {
- const data = mqmSigtestsData.metricData[metric];
- const systems = data.systems;
- const metricDoneUpdate = {
- metric: metric,
- metricDone: true,
- };
- /** We should have at least 2 systems and 1 trial for signif. testing. */
- if (systems.length < 2 || mqmSigtestsData.numTrials < 1) {
- postMessage(metricDoneUpdate);
- continue;
- }
- const scoresBySystem = data.scoresBySystem;
- const commonPos = data.commonPosBySystemPair;
- const signMultiplier = data.lowerBetter ? 1.0 : -1.0;
-
- /** Score differences by system pair. */
- const mqmPARDiffs = {};
-
- const log2NumTrials = Math.log2(mqmSigtestsData.numTrials);
-
- for (const [rowIdx, baseline] of systems.entries()) {
- if (!mqmPARDiffs.hasOwnProperty(baseline)) {
- mqmPARDiffs[baseline] = {};
- }
- for (const [colIdx, system] of systems.entries()) {
- if (rowIdx >= colIdx) {
- /** We only fill in the upper triangle. */
- continue;
- }
- const numCommonSegs = commonPos[baseline][system].length;
- if (log2NumTrials > numCommonSegs) {
- /** Not enough permutations possible, do not compute. */
- continue;
- }
- if (!mqmPARDiffs[baseline].hasOwnProperty(system)) {
- mqmPARDiffs[baseline][system] = [];
- }
- for (let i = 0; i < mqmSigtestsData.numTrials; i++) {
- const diff = mqmPAROneTrial(data, baseline, system);
- /** This means no common segments are found. */
- if (diff == null) break;
- mqmPARDiffs[baseline][system].push(diff);
- }
-
- const realDiff = (signMultiplier *
- (scoresBySystem[system].score - scoresBySystem[baseline].score));
- /**
- * Real score differences should be non-negative since we are filling in
- * the upper triangle.
- */
- console.assert(realDiff >= 0.0, realDiff);
- let cnt = 0;
- for (const diff of mqmPARDiffs[baseline][system]) {
- /**
- * Count how many samples of the null distribution are greater than or
- * equal to the real difference. This corresponds to
- * 'alternative="greater"' in scipy's API at
- * https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.permutation_test.html.
- * Recall that a greater value than `realDiff` indicates a bigger
- * difference between `system` and `baseline`.
- */
- if (diff >= realDiff) {
- cnt += 1;
- }
- }
- const numTrials = mqmPARDiffs[baseline][system].length;
- const p = (cnt + 1) / (numTrials + 1);
- const update = {
- metric: metric,
- row: rowIdx,
- col: colIdx,
- pValue: p,
- numCommonSegs: numCommonSegs,
- };
- /** Send this p-value to the parent thread. */
- postMessage(update);
- }
- }
- postMessage(metricDoneUpdate);
- }
- postMessage(finishedUpdate);
-}
-
-/**
- * Upon receiving the message with mqmSigtestsData from the parent thread,
- * kick off the computations.
- */
-onmessage = mqmPAR;
diff --git a/mqm_viewer/mqm-viewer-lite.html b/mqm_viewer/mqm-viewer-lite.html
deleted file mode 100644
index a6c9693592a..00000000000
--- a/mqm_viewer/mqm-viewer-lite.html
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
-
-
-
-MQM Viewer
-
-
-
-
-
-
-
-
-
-
-
diff --git a/mqm_viewer/mqm-viewer.js b/mqm_viewer/mqm-viewer.js
deleted file mode 100644
index 9c31b235bd1..00000000000
--- a/mqm_viewer/mqm-viewer.js
+++ /dev/null
@@ -1,4822 +0,0 @@
-// Copyright 2023 The Google Research Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-/**
- * This file contains the JavaScript code for MQM Viewer.
- */
-
-/**
- * Raw data read from the data file. Each entry is an array with 10 entries,
- * in this order (slightly different from the original order in the TSV data,
- * as we keep the fields in their more natural presentation order, as used in
- * the HTML table we display):
- *
- * 0: system, 1: doc, 2: docSegId, 3: globalSegId, 4: source, 5: target,
- * 6: rater, 7: category, 8: severity, 9: metadata
- *
- * The docSegId field is the 1-based index of the segment within the doc.
- *
- * The globalSegId field is an arbitrary, application-specific segment
- * identifier. If such an identifier is not needed or available, then set this
- * field to some constant value, such as 0. It is ignored by MQM Viewer, but is
- * available for use in filter expressions.
- *
- * The last field, "metadata", is an object that includes the timestamp of
- * the rating, any note the rater may have left, and other metadata.
- *
- * There is a special severity, "HOTW-test", reserved for hands-on-the-wheel
- * test results. These are test sentences in which a deliberate error is
- * injected, just to help the rater stay on task. The test result is captured
- * by category = "Found" or "Missed" and there is a note in the metadata that
- * captures the injected error.
- */
-let mqmData = [];
-
-/**
- * A data structure that provides a convenient way to iterate over mqmData in
- * nested loops on doc, docSegId, system.
- */
-let mqmDataIter = {
- docs: [],
- docSegs: {},
- docSys: {},
- docSegSys: {},
- systems: [], /** Convenient list of all systems in the data. */
-};
-
-/**
- * mqmDataFiltered has exactly the same format as mqmData, except that it
- * is limited to the current filters in place. It contains its metadata field
- * in its JSON-encoded form.
- */
-let mqmDataFiltered = [];
-
-/** Array indices in each mqmData */
-const MQM_DATA_SYSTEM = 0;
-const MQM_DATA_DOC = 1;
-const MQM_DATA_DOC_SEG_ID = 2;
-const MQM_DATA_GLOBAL_SEG_ID = 3;
-const MQM_DATA_SOURCE = 4;
-const MQM_DATA_TARGET = 5;
-const MQM_DATA_RATER = 6;
-const MQM_DATA_CATEGORY = 7;
-const MQM_DATA_SEVERITY = 8;
-const MQM_DATA_METADATA = 9;
-const MQM_DATA_NUM_PARTS = 10;
-
-/** Column filter id mappings */
-const mqmFilterColumns = {
- 'mqm-filter-doc': MQM_DATA_DOC,
- 'mqm-filter-doc-seg': MQM_DATA_DOC_SEG_ID,
- 'mqm-filter-system': MQM_DATA_SYSTEM,
- 'mqm-filter-source': MQM_DATA_SOURCE,
- 'mqm-filter-target': MQM_DATA_TARGET,
- 'mqm-filter-rater': MQM_DATA_RATER,
- 'mqm-filter-category': MQM_DATA_CATEGORY,
- 'mqm-filter-severity': MQM_DATA_SEVERITY,
-};
-
-/**
- * If TSV data was supplied (instead of being chosen from a file), then it is
- * saved here (for possible downloading).
- */
-let mqmTSVData = '';
-
-/**
- * The first two mqmStats* objects are keyed by [system][doc][docSegId]. Apart
- * from all the systems, an additional, special system value ('_MQM_TOTAL_') is
- * used in both, for aggregates over all systems (for this aggregate, the
- * doc key used is doc:system). mqmStatsByRater is first keyed by [rater] (and
- * then by [system][doc][docSegId]). Each keyed entry is an array of per-rater
- * stats (scores, score slices, error counts) for that segment
- * (system+doc+docSegId).
- *
- * mqmSevCatStats[severity][category][system] is the total count of
- * annotations of a specific severity+category in a specific system.
- *
- * Each mqmStats* object is recomputed for any filtering applied to the data.
- */
-let mqmStats = {};
-let mqmStatsByRater = {};
-let mqmSevCatStats = {};
-
-/** {!Element} HTML table body elements for various tables */
-let mqmTable = null;
-let mqmStatsTable = null;
-let mqmSevCatStatsTable = null;
-let mqmEventsTable = null;
-
-/** Events timing info for current filtered data. **/
-let mqmEvents = {};
-
-/**
- * Max number of annotations to show in the sample of ratings shown. Note that
- * this is not a hard limit, as we include all systems + raters for any
- * document segment that pass the current filter (if any).
- */
-let mqmLimit = 200;
-
-/** Clause built by helper menus, for appending to the filter expression **/
-let mqmClause = '';
-
-/** UI elements for clause-builder. */
-let mqmClauseKey;
-let mqmClauseInclExcl;
-let mqmClauseSev;
-let mqmClauseCat;
-let mqmClauseAddAnd;
-let mqmClauseAddOr;
-
-/** Selected system names for system-v-system comparison. */
-let mqmSysVSys1;
-let mqmSysVSys2;
-
-/** A distinctive name used as the key for aggregate stats. */
-const MQM_TOTAL = '_MQM_TOTAL_';
-
-const MQM_PVALUE_THRESHOLD = 0.05;
-const MQM_SIGTEST_TRIALS = 10000;
-
-/**
- * An object that captures all the data needed for running signigicance
- * tests on one particular metric.
- */
-function MQMMetricSigtestsData() {
- /** {boolean} */
- this.lowerBetter = false;
- /**
- * {!Array} Sorted array ordered by degrading scores.
- */
- this.systems = [];
- /**
- * {!Object} Scores by system. Each score itself is an object containing
- * score and scoreDenominator.
- */
- this.scoresBySystem = {};
- /**
- * {!Object} Segment scores by system. Each value is an array of scores that
- * are aligned such that elements at the n-th position of all arrays
- * correspond to the same segment. Note that some scores might be null
- * since some systems might be missing ratings for some segments.
- */
- this.segScoresBySystem = {};
- /**
- * {!Object} Common segments shared by a pair of systems. This stores
- * positions in segScoresBySystem.
- */
- this.commonPosBySystemPair = {};
- /** {!Array>} Computed matric of p-values. */
- this.pValues = [];
-}
-
-/**
- * An object with data for computing significance tests. This data is sent to a
- * background Worker thread. See computation details in mqm-sigtests.js. The
- * object metricData[] has one entry for each metric in mqmMetricsVisible[].
- */
-let mqmSigtestsData = {
- metricData: {},
- /** {number} Number of trials. */
- numTrials: MQM_SIGTEST_TRIALS,
-};
-
-/** {!Worker} A background Worker thread that computes sigtests */
-let mqmSigtestsWorker = null;
-/**
- * The Sigtests Worker loads its code from 'mqm-sigtests.js'. If that file is
- * not servable for some reason, then set the mqmSigtestsWorkerJS variable
- * to its contents.
- */
-let mqmSigtestsWorkerJS = '';
-/** {!Element} An HTML span that shows a sigtests computation status message. */
-let mqmSigtestsMsg = null;
-
-/**
- * Scoring weights. Each weight has a name and a regular expression pattern
- * for matching :[/] (case-insensitively).
- * The weights are tried out in the sequence shown and for a given annotation,
- * the first matching weight is used. While you can interactively change these
- * for experimentation, you should set this default array to values suitable
- * for your application. The best place to do this is in your own version of
- * mqm-viewer.html.
- *
- * The "name" fields should be unique, short (<= 10 characters), and composed
- * only of [a-zA-Z-] (no periods please).
- */
-let mqmDefaultWeights = [
- {
- 'name': 'Trivial',
- 'weight': 0.1,
- 'pattern': 'minor:.*punctuation|trivial:',
- },
- {
- 'name': 'Creative',
- 'weight': 0,
- 'pattern': ':.*reinterpretation',
- },
- {
- 'name': 'Source',
- 'weight': 0,
- 'pattern': ':source',
- },
- {
- 'name': 'Non-trans',
- 'weight': 25,
- 'pattern': 'non.translation',
- },
- {
- 'name': 'Minor',
- 'weight': 1,
- 'pattern': 'minor:',
- },
- {
- 'name': 'Major',
- 'weight': 5,
- 'pattern': 'major:',
- },
- {
- 'name': 'Critical',
- 'weight': 5,
- 'pattern': 'critical:',
- },
-];
-
-/**
- * MQM Scores can be sliced along a second dimension, which is typically
- * Accuracy/Fluency, but can be customized in any desired manner. The
- * slicing is done by matching the pattern regular expressions
- * case-insensitively in order to find the first matching slice. Similarly
- * as with mqmDefaultWeights, you may want to override these defaults in
- * your own application.
- *
- * See comment above mqmDefaultWeights for requirements on the "name" field.
- */
-let mqmDefaultSlices = [
- {
- 'name': 'Accuracy',
- 'pattern': 'accuracy|terminology|non.translation',
- },
- {
- 'name': 'Fluency',
- 'pattern': 'fluency|style|locale',
- },
- {
- 'name': 'Other',
- 'pattern': '.*',
- },
-];
-
-/**
- * mqmWeights and mqmSlices are set from current settings in
- * mqmParseScoreSettings() and mqmResetSettings().
- */
-let mqmWeights = [];
-let mqmSlices = [];
-
-/**
- * Score aggregates include 'mqm-weighted-" and "mqm-slice-" prefixed
- * scores. The names beyond the prefixes are taken from the "name" field in
- * mqmWeights and mqmSlices.
- */
-const MQM_SCORE_WEIGHTED_PREFIX = 'mqm-weighted-';
-const MQM_SCORE_SLICE_PREFIX = 'mqm-slice-';
-
-/**
- * Arrays of names of currently being displayed score components, sorted in
- * decreasing score order.
- */
-let mqmScoreWeightedFields = [];
-let mqmScoreSliceFields = [];
-
-/**
- * Scoring unit. If false, segments are used for scoring. If true, scores
- * are computed per "100 source characters".
- */
-let mqmCharScoring = false;
-
-/**
- * The field to sort the score table rows by. By default, sort by
- * overall MQM score. `mqmSortReverse` indicates whether it is sorted in
- * ascending order (false, default) or descending order (true).
- *
- * The value of this is something like 'metric-' (where k is an index into
- * mqmMetrics[]), or a name from mqmSoreWeightedFields[]/mqmScoreSliceFields[].
- */
-let mqmSortByField = 'metric-0';
-let mqmSortReverse = false;
-
-/**
- * All metrics possibly available in the current data. The entries will be like
- * 'MQM', 'BLEURT-X', etc. 'MQM' is the always the first entry in this array.
- * {!Array} Indices into mqmMetrics.
- */
-let mqmMetrics = ['MQM'];
-/**
- * Info about metrics.
- */
-const mqmMetricsInfo = {
- 'MQM': {
- index: 0, /** index into mqmMetrics[] */
- lowerBetter: true, /** default is false */
- },
-};
-/**
- * The metrics that are available for the data with the current filtering.
- * {!Array} Indices into mqmMetrics.
- */
-let mqmMetricsVisible = [];
-
-/**
- * Listener for changes to the input field that specifies the limit on
- * the number of rows shown.
- */
-function setMqmLimit() {
- const limitElt = document.getElementById('mqm-limit');
- const limit = limitElt.value.trim();
- if (limit > 0) {
- mqmLimit = limit;
- mqmShow();
- } else {
- limitElt.value = mqmLimit;
- }
-}
-
-/**
- * This function returns a "comparable" version of docSegId by padding it
- * with leading zeros. When docSegId is a non-negative integer (reasonably
- * bounded), then this ensures numeric ordering.
- * @param {string} s
- * @return {string}
- */
-function mqmCmpDocSegId(s) {
- return ('' + s).padStart(10, '0');
-}
-
-/**
- * This sorts 10-column MQM data by fields in the order doc, docSegId, system,
- * rater, severity, category.
- * @param {!Array} data The MQM-10-column data to be sorted.
- */
-function mqmSortData(data) {
- data.sort((e1, e2) => {
- let diff = 0;
- const docSegId1 = mqmCmpDocSegId(e1[MQM_DATA_DOC_SEG_ID]);
- const docSegId2 = mqmCmpDocSegId(e2[MQM_DATA_DOC_SEG_ID]);
- if (e1[MQM_DATA_DOC] < e2[MQM_DATA_DOC]) {
- diff = -1;
- } else if (e1[MQM_DATA_DOC] > e2[MQM_DATA_DOC]) {
- diff = 1;
- } else if (docSegId1 < docSegId2) {
- diff = -1;
- } else if (docSegId1 > docSegId2) {
- diff = 1;
- } else if (e1[MQM_DATA_SYSTEM] < e2[MQM_DATA_SYSTEM]) {
- diff = -1;
- } else if (e1[MQM_DATA_SYSTEM] > e2[MQM_DATA_SYSTEM]) {
- diff = 1;
- } else if (e1[MQM_DATA_RATER] < e2[MQM_DATA_RATER]) {
- diff = -1;
- } else if (e1[MQM_DATA_RATER] > e2[MQM_DATA_RATER]) {
- diff = 1;
- } else if (e1[MQM_DATA_SEVERITY] < e2[MQM_DATA_SEVERITY]) {
- diff = -1;
- } else if (e1[MQM_DATA_SEVERITY] > e2[MQM_DATA_SEVERITY]) {
- diff = 1;
- } else if (e1[MQM_DATA_CATEGORY] < e2[MQM_DATA_CATEGORY]) {
- diff = -1;
- } else if (e1[MQM_DATA_CATEGORY] > e2[MQM_DATA_CATEGORY]) {
- diff = 1;
- }
- return diff;
- });
-}
-
-/**
- * Sets mqmDataIter to a data structure that can be used to iterate over
- * mqmData[] rows by looping over documents, segments, and systems.
- */
-function mqmCreateDataIter() {
- mqmDataIter = {
- docs: [],
- docSegs: {},
- docSys: {},
- docSegSys: {},
- systems: [],
- evaluation: {},
- };
- let lastRow = null;
- const systemsSet = new Set();
- for (let rowId = 0; rowId < mqmData.length; rowId++) {
- const parts = mqmData[rowId];
- const doc = parts[MQM_DATA_DOC];
- const docSegId = parts[MQM_DATA_DOC_SEG_ID];
- const system = parts[MQM_DATA_SYSTEM];
- systemsSet.add(system);
- const sameDoc = lastRow && (doc == lastRow[MQM_DATA_DOC]);
- const sameDocSeg = sameDoc && (docSegId == lastRow[MQM_DATA_DOC_SEG_ID]);
- const sameDocSys = sameDoc && (system == lastRow[MQM_DATA_SYSTEM]);
- if (!sameDoc) {
- mqmDataIter.docs.push(doc);
- mqmDataIter.docSegs[doc] = [];
- mqmDataIter.docSys[doc] = [];
- }
- if (!sameDocSeg) {
- console.assert(!mqmDataIter.docSegs[doc].includes(docSegId),
- doc, docSegId);
- mqmDataIter.docSegs[doc].push(docSegId);
- }
- if (!sameDocSys && !mqmDataIter.docSys[doc].includes(system)) {
- mqmDataIter.docSys[doc].push(system);
- }
- lastRow = parts;
- }
- mqmDataIter.systems = [...systemsSet];
- /**
- * Ensure that there are entries in docSegSys for each
- * docSegId x system.
- */
- for (doc of mqmDataIter.docs) {
- mqmDataIter.docSegSys[doc] = {};
- for (docSegId of mqmDataIter.docSegs[doc]) {
- mqmDataIter.docSegSys[doc][docSegId] = {};
- for (system of mqmDataIter.systems) {
- mqmDataIter.docSegSys[doc][docSegId][system] = {
- rows: [-1, -1],
- segment: {},
- };
- }
- }
- }
- lastRow = null;
- let segment = null;
- for (let rowId = 0; rowId < mqmData.length; rowId++) {
- const parts = mqmData[rowId];
- const doc = parts[MQM_DATA_DOC];
- const docSegId = parts[MQM_DATA_DOC_SEG_ID];
- const system = parts[MQM_DATA_SYSTEM];
- const metadata = parts[MQM_DATA_METADATA];
- if (metadata.evaluation) {
- mqmDataIter.evaluation = {
- ...mqmDataIter.evaluation,
- ...metadata.evaluation
- };
- }
- const sameDoc = lastRow && (doc == lastRow[MQM_DATA_DOC]);
- const sameDocSeg = sameDoc && (docSegId == lastRow[MQM_DATA_DOC_SEG_ID]);
- const sameDocSegSys = sameDocSeg && (system == lastRow[MQM_DATA_SYSTEM]);
-
- if (!sameDocSegSys) {
- mqmDataIter.docSegSys[doc][docSegId][system].rows =
- [rowId, rowId + 1];
- segment = metadata.segment || {};
- } else {
- mqmDataIter.docSegSys[doc][docSegId][system].rows[1] = rowId + 1;
- }
- mqmDataIter.docSegSys[doc][docSegId][system].segment = segment;
- lastRow = parts;
- }
-}
-
-/**
- * If obj does not have an array property named key, creates an empty array.
- * Pushes val into the obj[key] array.
- * @param {!Object} obj
- * @param {string} key
- * @param {string} val
- */
-function mqmAddToArray(obj, key, val) {
- if (!obj.hasOwnProperty(key)) obj[key] = [];
- obj[key].push(val);
-}
-
-
-/**
- * Returns the location of elt in sorted array arr using binary search. if
- * elt is not present in arr, then returns the slot where it belongs in sorted
- * order.
- * @param {!Array} arr Sorted array of numbers.
- * @param {number} elt
- * @return {number}
- */
-function mqmBinSearch(arr, elt) {
- let l = 0;
- let r = arr.length;
- while (l < r) {
- const m = Math.floor((l + r) / 2);
- if (arr[m] < elt) {
- l = m + 1;
- } else {
- r = m;
- }
- }
- return l;
-}
-
-/**
- * Given an array of all instances of annotated text for a segment (where
- * annotations have been marked using .. spans), generates a
- * tokenization that starts with space-based splitting, but refines it to
- * ensure that each and is at a token boundary. Returns the
- * tokenization as well as an array containing the marked spans encoded as
- * [start, end] token indices (both inclusive).
- *
- * The structure of the returned object is: {
- * tokens: !Array,
- * spans: !Array>
- * }
- * @param {!Array} annotations
- * @return {!Object}
- */
-function mqmTokenizeLegacyText(annotations) {
- let cleanText = '';
- for (let text of annotations) {
- const noMarkers = text.replace(/<\/?v>/g, '');
- if (noMarkers.length > cleanText.length) {
- cleanText = noMarkers;
- }
- }
- const spacedTokens = cleanText.split(' ');
- const tokens = [];
- for (let i = 0; i < spacedTokens.length; i++) {
- tokens.push(spacedTokens[i]);
- tokens.push(' ');
- }
- const tokenOffsets = [];
- let tokenOffset = 0;
- for (let token of tokens) {
- tokenOffsets.push(tokenOffset);
- tokenOffset += token.length;
- }
-
- const MARKERS = ['', ''];
- const markerOffsets = [];
- for (let text of annotations) {
- const offsets = [];
- let markerIdx = 0;
- let modText = text;
- let x;
- while ((x = modText.indexOf(MARKERS[markerIdx])) >= 0) {
- const marker = MARKERS[markerIdx];
- offsets.push(x);
- modText = modText.substr(0, x) + modText.substr(x + marker.length);
- markerIdx = 1 - markerIdx;
-
- const loc = mqmBinSearch(tokenOffsets, x);
- if (tokenOffsets.length > loc && tokenOffsets[loc] == x) {
- continue;
- }
- /**
- * The current marker ( or ) lies inside a token. Split that
- * token.
- */
- const toSplit = loc - 1;
- if (toSplit < 0) {
- console.log('Weird splitting situation for offset: ' + x +
- ' in [' + modText + ']');
- continue;
- }
- console.assert(toSplit < tokenOffsets.length);
- console.assert(tokenOffsets[toSplit] < x);
- const oldToken = tokens[toSplit];
- console.assert(tokenOffsets[toSplit] + oldToken.length > x);
- const newLen = x - tokenOffsets[toSplit];
- tokens[toSplit] = oldToken.substr(0, newLen);
- tokens.splice(loc, 0, oldToken.substr(newLen));
- tokenOffsets.splice(loc, 0, x);
- }
- markerOffsets.push(offsets);
- }
- const spansList = [];
- for (let offsets of markerOffsets) {
- const spans = [];
- for (let i = 0; i < offsets.length; i+= 2) {
- if (i + 1 >= offsets.length) break;
- spans.push([mqmBinSearch(tokenOffsets, offsets[i]),
- mqmBinSearch(tokenOffsets, offsets[i + 1]) - 1]);
- }
- spansList.push(spans);
- }
- return {
- tokens: tokens,
- spans: spansList,
- };
-}
-
-/**
- * Given the full range of rows for the same doc+docSegId+system, tokenizes the
- * source and target side using spaces, but refining the tokenization to make
- * each and fall on a token boundary. Sets
- * segment.{source,target}_tokens as well as
- * mqmData[row][MQM_DATA_METADATA].{source,target}_spans.
- *
- * If segment.source/target_tokens is already present in the data (as
- * will be the case with newer data), this function is a no-op.
- * If rowRange does not cover any rows, then this function is a no-op.
- * @param {!Array} rowRange The start (inclusive) and limit (exclusive)
- * rowId for the segment, in mqmData[].
- * @param {!Object} segment The segment-level aggregate data.
- */
-function mqmTokenizeLegacySegment(rowRange, segment) {
- if (rowRange[0] >= rowRange[1]) {
- return;
- }
- const sources = [];
- const targets = [];
- for (let row = rowRange[0]; row < rowRange[1]; row++) {
- const parts = mqmData[row];
- sources.push(parts[MQM_DATA_SOURCE]);
- targets.push(parts[MQM_DATA_TARGET]);
- }
- const sourceTokenization = mqmTokenizeLegacyText(sources);
- segment.source_tokens = sourceTokenization.tokens;
- const targetTokenization = mqmTokenizeLegacyText(targets);
- segment.target_tokens = targetTokenization.tokens;
- for (let row = rowRange[0]; row < rowRange[1]; row++) {
- const parts = mqmData[row];
- const idx = row - rowRange[0];
- parts[MQM_DATA_METADATA].source_spans = sourceTokenization.spans[idx];
- parts[MQM_DATA_METADATA].target_spans = targetTokenization.spans[idx];
- }
-}
-
-/**
- * Aggregates mqmData, collecting all data for a particular segment translation
- * (i.e., for a given (doc, docSegId) pair) into the aggrDocSeg object in
- * the metadata.segment field, adding to it the following properties:
- * {cats,sevs,sevcats}By{Rater,System}.
- * Each of these properties is an object keyed by system or rater, with the
- * values being arrays of strings that are categories, severities,
- * and [/], * respectively.
- *
- * Also added are aggrDocSeg.metrics[metric][system] values.
- * Makes sure that the metadata.segment object is common for each row from
- * the same doc+seg+sys.
- */
-function mqmAddSegmentAggregations() {
- for (doc of mqmDataIter.docs) {
- const aggrDoc = {
- doc: doc,
- thumbsUpCount: 0,
- thumbsDownCount: 0,
- };
- for (docSegId of mqmDataIter.docSegs[doc]) {
- aggrDocSeg = {
- catsBySystem: {},
- catsByRater: {},
- sevsBySystem: {},
- sevsByRater: {},
- sevcatsBySystem: {},
- sevcatsByRater: {},
- metrics: {},
- aggrDoc: aggrDoc,
- };
- for (system of mqmDataIter.docSys[doc]) {
- const range = mqmDataIter.docSegSys[doc][docSegId][system].rows;
- let aggrDocSegSys = {
- aggrDocSeg: aggrDocSeg,
- metrics: {},
- };
- for (let rowId = range[0]; rowId < range[1]; rowId++) {
- const parts = mqmData[rowId];
- const segment = parts[MQM_DATA_METADATA].segment || {};
- if (segment.hasOwnProperty('metrics')) {
- aggrDocSegSys.metrics = {
- ...segment.metrics,
- ...aggrDocSegSys.metrics,
- };
- }
- aggrDocSegSys = {...segment, ...aggrDocSegSys};
- }
- for (metric in aggrDocSegSys.metrics) {
- if (!aggrDocSeg.metrics.hasOwnProperty(metric)) {
- aggrDocSeg.metrics[metric] = {};
- }
- aggrDocSeg.metrics[metric][system] = aggrDocSegSys.metrics[metric];
- }
- if (!aggrDocSegSys.source_tokens ||
- aggrDocSegSys.source_tokens.length == 0) {
- mqmTokenizeLegacySegment(range, aggrDocSegSys);
- }
- if (!aggrDocSeg.hasOwnProperty('source_tokens') &&
- aggrDocSegSys.hasOwnProperty('source_tokens')) {
- aggrDocSeg.source_tokens = aggrDocSegSys.source_tokens;
- }
- if (!aggrDocSeg.hasOwnProperty('source_sentence_tokens') &&
- aggrDocSegSys.hasOwnProperty('source_sentence_tokens')) {
- aggrDocSeg.source_sentence_tokens =
- aggrDocSegSys.source_sentence_tokens;
- }
- if (!aggrDocSeg.hasOwnProperty('starts_paragraph') &&
- aggrDocSegSys.hasOwnProperty('starts_paragraph')) {
- aggrDocSeg.starts_paragraph = aggrDocSegSys.starts_paragraph;
- }
- if (aggrDocSegSys.hasOwnProperty('references')) {
- if (!aggrDocSeg.hasOwnProperty('references')) {
- aggrDocSeg.references = {};
- }
- aggrDocSeg.references = {
- ...aggrDocSeg.references,
- ...aggrDocSegSys.references
- };
- }
- if (!aggrDocSeg.hasOwnProperty('primary_reference') &&
- aggrDocSegSys.hasOwnProperty('primary_reference')) {
- aggrDocSeg.primary_reference = aggrDocSegSys.primary_reference;
- }
- for (let rowId = range[0]; rowId < range[1]; rowId++) {
- const parts = mqmData[rowId];
- const metadata = parts[MQM_DATA_METADATA];
- metadata.segment = aggrDocSegSys;
- mqmSetMarkedText(rowId, metadata);
-
- const rater = parts[MQM_DATA_RATER];
- if (!rater) {
- /**
- * This row is purely for metadata, such as references and/or
- * automated metrics
- */
- continue;
- }
- const category = parts[MQM_DATA_CATEGORY];
- const severity = parts[MQM_DATA_SEVERITY];
-
- mqmAddToArray(aggrDocSeg.catsBySystem, system, category);
- mqmAddToArray(aggrDocSeg.catsByRater, rater, category);
- mqmAddToArray(aggrDocSeg.sevsBySystem, system, severity);
- mqmAddToArray(aggrDocSeg.sevsByRater, rater, severity);
- const sevcat = severity + (category ? '/' + category : '');
- mqmAddToArray(aggrDocSeg.sevcatsBySystem, system, sevcat);
- mqmAddToArray(aggrDocSeg.sevcatsByRater, rater, sevcat);
- if (metadata.feedback && metadata.feedback.thumbs) {
- if (metadata.feedback.thumbs == 'up') {
- aggrDoc.thumbsUpCount++;
- } else if (metadata.feedback.thumbs == 'down') {
- aggrDoc.thumbsDownCount++;
- }
- }
- if (metadata.feedback && metadata.feedback.notes) {
- aggrDoc.feedbackNotes = (aggrDoc.feedbackNotes || '') +
- metadata.feedback.notes;
- }
- }
- }
- }
- }
-}
-
-/**
- * Returns an object consisting of filterREs (a dictionary of column
- * filter REs keyed by the id of the filter), filterExpr (a JavaScript
- * expression for filtering, possibly entered by the user), and onlyAllSysSegs
- * (a boolean that captures the value of the corresponding checkbox).
- *
- * Also sets the value of the select menus for column filters (if they exist).
- * @return {!Object}
- */
-function mqmGetAllFilters() {
- const res = {};
- const filters = document.getElementsByClassName('mqm-filter-re');
- for (let i = 0; i < filters.length; i++) {
- const filter = filters[i].value.trim();
- const id = filters[i].id;
- const selectId = id.replace(/filter/, 'select');
- const sel = document.getElementById(selectId);
- if (sel) sel.value = filter;
- if (!filter) {
- res[id] = null;
- continue;
- }
- res[id] = new RegExp(filter);
- }
- const filterExpr = document.getElementById('mqm-filter-expr').value.trim();
- const onlyAllSysSegs = document.getElementById(
- 'mqm-only-all-systems-segments').checked;
- return {
- filterREs: res,
- filterExpr: filterExpr,
- onlyAllSysSegs: onlyAllSysSegs,
- };
-}
-
-/**
- * Short-cut function (convenient in filter expressions) for:
- * "obj has array property prop that includes val"
- * @param {!Object} obj
- * @param {string} prop
- * @param {string} val
- * @return {boolean}
- */
-function mqmIncl(obj, prop, val) {
- return obj.hasOwnProperty(prop) &&
- obj[prop].includes(val);
-}
-
-/**
- * Short-cut function (convenient in filter expressions) for:
- * "obj has array property prop that excludes val"
- * @param {!Object} obj
- * @param {string} prop
- * @param {string} val
- * @return {boolean}
- */
-function mqmExcl(obj, prop, val) {
- return obj.hasOwnProperty(prop) &&
- !obj[prop].includes(val);
-}
-
-/**
- * Clears mqmClause and disables associated buttons.
- */
-function mqmClearClause() {
- mqmClause = '';
- mqmClauseKey.value = '';
- mqmClauseInclExcl.value = 'includes';
- mqmClauseSev.value = '';
- mqmClauseCat.value = '';
- mqmClauseAddAnd.disabled = true;
- mqmClauseAddOr.disabled = true;
-}
-
-/**
- * Checks if filter expression clause is fully specified, enables "add"
- * buttons if so.
- */
-function mqmCheckClause() {
- mqmClause = '';
- mqmClauseAddAnd.disabled = true;
- mqmClauseAddOr.disabled = true;
- if (!mqmClauseKey.value) return;
- if (!mqmClauseSev.value && !mqmClauseCat.value) return;
-
- let sevcats = 'aggrDocSeg.sevcats';
- let key = '';
- let err = mqmClauseSev.value + '/' + mqmClauseCat.value;
- if (!mqmClauseSev.value) {
- sevcats = 'aggrDocSeg.cats';
- err = mqmClauseCat.value;
- }
- if (!mqmClauseCat.value) {
- sevcats = 'aggrDocSeg.sevs';
- err = mqmClauseSev.value;
- }
- if (mqmClauseKey.value.startsWith('System: ')) {
- sevcats += 'BySystem';
- key = mqmClauseKey.value.substr(8);
- } else {
- console.assert(mqmClauseKey.value.startsWith('Rater: '),
- mqmClauseKey.value);
- sevcats += 'ByRater';
- key = mqmClauseKey.value.substr(7);
- }
- const inclexcl = (mqmClauseInclExcl.value == 'excludes') ? 'mqmExcl' :
- 'mqmIncl';
- mqmClause = `${inclexcl}(${sevcats}, "${key}", "${err}")`;
- mqmClauseAddAnd.disabled = false;
- mqmClauseAddOr.disabled = false;
-}
-
-/**
- * Adds mqmClause with and/or to the filter expression.
- * @param {string} andor
- */
-function mqmAddClause(andor) {
- if (!mqmClause) return;
- const elt = document.getElementById('mqm-filter-expr');
- let expr = elt.value.trim();
- if (expr) expr += ' ' + andor + ' ';
- expr += mqmClause;
- elt.value = expr;
- mqmClearClause();
- mqmShow();
-}
-
-/**
- * Evaluates the JavaScript filterExpr on an mqmData[] row and returns true
- * only if the filter passes.
- * @param {string} filterExpr
- * @param {!Array} parts
- * @return {boolean}
- */
-function mqmFilterExprPasses(filterExpr, parts) {
- if (!filterExpr.trim()) return true;
- try {
- return Function(
- '"use strict";' +
- `
- const system = arguments[MQM_DATA_SYSTEM];
- const doc = arguments[MQM_DATA_DOC];
- const docSegId = arguments[MQM_DATA_DOC_SEG_ID];
- const globalSegId = arguments[MQM_DATA_GLOBAL_SEG_ID];
- const source = arguments[MQM_DATA_SOURCE];
- const target = arguments[MQM_DATA_TARGET];
- const rater = arguments[MQM_DATA_RATER];
- const category = arguments[MQM_DATA_CATEGORY];
- const severity = arguments[MQM_DATA_SEVERITY];
- const metadata = arguments[MQM_DATA_METADATA];
- const segment = metadata.segment;
- const aggrDocSegSys = segment;
- const aggrDocSeg = aggrDocSegSys.aggrDocSeg;
- const aggrDoc = aggrDocSeg.aggrDoc;` +
- 'return (' + filterExpr + ')')(
- parts[MQM_DATA_SYSTEM], parts[MQM_DATA_DOC],
- parts[MQM_DATA_DOC_SEG_ID], parts[MQM_DATA_GLOBAL_SEG_ID],
- parts[MQM_DATA_SOURCE], parts[MQM_DATA_TARGET],
- parts[MQM_DATA_RATER], parts[MQM_DATA_CATEGORY],
- parts[MQM_DATA_SEVERITY], parts[MQM_DATA_METADATA]);
- } catch (err) {
- document.getElementById('mqm-filter-expr-error').innerHTML = err;
- return false;
- }
-}
-
-/**
- * This function will return false for segments that have some metric (MQM or
- * other) only available for some of the systems, not all/none.
- * @param {!Object} metadata
- * @return {boolean}
- */
-function mqmAllSystemsFilterPasses(metadata) {
- const segment = metadata.segment;
- const aggrDocSeg = segment.aggrDocSeg;
- for (let metric in aggrDocSeg.metrics) {
- const numSystemsWithMetric = Object.keys(aggrDocSeg.metrics[metric]).length;
- if (numSystemsWithMetric > 0 &&
- numSystemsWithMetric != mqmDataIter.systems.length) {
- return false;
- }
- }
- return true;
-}
-
-/**
- * Logs the metadata from one particular row to the JavaScript console. The
- * row number is provided by the user in an element. This is
- * useful when formulating filter functions, to see what metadata fields are
- * available.
- */
-function mqmLogRowMetadata() {
- const rowInput = document.getElementById('mqm-view-metadata-row');
- const rowInputVal = rowInput.value.trim();
- if (!rowInputVal) return;
- const row = parseInt(rowInputVal);
- if (row < 0 || row >= mqmData.length) {
- console.log(`Row must be in the range 0-${mqmData.length - 1}`);
- rowInput.value = '';
- return;
- }
- const doc = mqmData[row][MQM_DATA_DOC];
- const docSegId = mqmData[row][MQM_DATA_DOC_SEG_ID];
- const system = mqmData[row][MQM_DATA_SYSTEM];
- const rater = mqmData[row][MQM_DATA_RATER];
- console.log('Metadata for row ' + row +
- ' - doc [' + doc + '], docSegId [' + docSegId +
- '], system [' + system + '], rater [' + rater + ']:');
- console.log(mqmData[row][MQM_DATA_METADATA]);
- console.log('Note that aggrDocSegSys is an alias for metadata.segment, ' +
- 'aggrDocSeg for aggrDocSegSys.aggrDocSeg, ' +
- 'and aggrDoc for aggrDocSeg.aggrDoc');
-}
-
-/**
- * In the weights/slices settings table with the given element id, add a row.
- * @param {string} id
- * @param {number} cols The number of columns to use.
- */
-function mqmSettingsAddRow(id, cols) {
- let html = '
';
- for (let i = 0; i < cols; i++) {
- html += `
-
`;
- }
- html += '
';
- const elt = document.getElementById(id);
- const rowNum = document.getElementById(id + '-add-row').value ?? 1;
- /** The new row needs to be the 1-based position "rowNum" */
- const rows = elt.getElementsByTagName('tr');
- if (rows.length == 0 || rowNum <= 1) {
- elt.insertAdjacentHTML('afterbegin', html);
- } else {
- if (rowNum > rows.length) {
- rows[rows.length - 1].insertAdjacentHTML('afterend', html);
- } else {
- rows[rowNum - 1].insertAdjacentHTML('beforebegin', html);
- }
- }
-}
-
-/**
- * Displays settings tables for score weights and slices.
- */
-function mqmSetUpScoreSettings() {
- const weightSettings = document.getElementById('mqm-settings-weights');
- weightSettings.innerHTML = '';
- for (let sc of mqmWeights) {
- sc.regex = new RegExp(sc.pattern, 'i');
- weightSettings.insertAdjacentHTML('beforeend', `
-
-
${sc.name}
-
${sc.pattern}
-
${sc.weight}
-
`);
- }
- const sliceSettings = document.getElementById('mqm-settings-slices');
- sliceSettings.innerHTML = '';
- for (let sc of mqmSlices) {
- sc.regex = new RegExp(sc.pattern, 'i');
- sliceSettings.insertAdjacentHTML('beforeend', `
-
-
${sc.name}
-
${sc.pattern}
-
`);
- }
-}
-
-/**
- * Parses score weights/slices from the user-edited table identified by id.
- * If there are errors in parsing then they are displayed to the user in the
- * mqm-errors element and null is returned.
- * @param {string} id
- * @param {boolean} hasWeight True if this is the weights table.
- * @return {?Array} Array of parsed weights/slices, or null if errors.
- */
-function mqmParseScoreSettingsInner(id, hasWeight) {
- const nameChecker = new RegExp(/^[a-z0-9\.-]+$/i);
- const errorsFound = [];
- const rows = document.getElementById(id).getElementsByTagName('tr');
- const parsed = [];
- const names = {};
- for (let i = 0; i < rows.length; i++) {
- if (!rows[i].textContent.trim()) {
- /* Allow skipping blank lines */
- continue;
- }
- const parsedRow = {};
- const spans = rows[i].getElementsByTagName('span');
- console.assert(spans.length == 2 + (hasWeight ? 1 : 0), spans);
- parsedRow.name = spans[0].textContent.trim();
- if (parsedRow.name.length > 10) {
- errorsFound.push(
- 'The name [' + parsedRow.name + '] is longer than 10 chars');
- continue;
- }
- if (!nameChecker.test(parsedRow.name)) {
- errorsFound.push(
- 'The name [' + parsedRow.name +
- '] cannot be empty, must use characters [a-zA-Z0-9.-]');
- continue;
- }
- if (names[parsedRow.name]) {
- errorsFound.push('The name [' + parsedRow.name + '] is a duplicate');
- continue;
- }
- parsedRow.pattern = spans[1].textContent.trim();
- try {
- parsedRow.regex = new RegExp(parsedRow.pattern, 'i');
- } catch (err) {
- console.log(err);
- parsedRow.pattern = '';
- }
- if (!parsedRow.pattern) {
- errorsFound.push(
- 'The pattern in row [' + parsedRow.name + '] is empty/invalid');
- continue;
- }
- if (hasWeight) {
- parsedRow.weight = parseFloat(spans[2].textContent.trim());
- if (isNaN(parsedRow.weight) || parsedRow.weight < 0) {
- errorsFound.push(
- 'The weight in row [' + parsedRow.name + '] is invalid');
- continue;
- }
- }
- /* All good! */
- names[parsedRow.name] = true;
- parsed.push(parsedRow);
- }
- const errorsElt = document.getElementById('mqm-errors');
- for (let error of errorsFound) {
- errorsElt.insertAdjacentHTML('beforeend', `
${error}
\n`);
- }
- return (errorsFound.length == 0) ? parsed : null;
-}
-
-/**
- * Parses score weights and slices from the user-edited settings tables. Sets
- * mqmWeights and mqmSlices if successful.
- * @return {boolean} True if the parsing was successful.
- */
-function mqmParseScoreSettings() {
- const errors = document.getElementById('mqm-errors');
- errors.innerHTML = '';
- const newWeights = mqmParseScoreSettingsInner('mqm-settings-weights', true);
- const newSlices = mqmParseScoreSettingsInner('mqm-settings-slices', false);
- if (!newWeights || !newSlices) {
- return false;
- }
- mqmWeights = newWeights;
- mqmSlices = newSlices;
- return true;
-}
-
-/**
- * This checks if the annotation matches the pattern in the score weight/slice
- * component.
- * @param {!Object} sc Score component, with a regex property.
- * @param {string} sev Severity of the annotation.
- * @param {string} cat Category (and optional "/" + subcat.) of the annotation.
- * @return {boolean}
- */
-function mqmMatchesScoreSplit(sc, sev, cat) {
- return sc.regex.test(sev + ':' + cat);
-}
-
-/**
- * Returns a string that shows the value of the metric to three decimal places.
- * If denominator is <= 0, then returns "-".
- * @param {number} metric
- * @param {number} denominator
- * @return {string}
- */
-function mqmMetricDisplay(metric, denominator) {
- return (denominator > 0) ? metric.toFixed(3) : '-';
-}
-
-/**
- * Initializes and returns a rater stats object.
- * @param {string} rater
- * @return {!Object}
- */
-function mqmInitRaterStats(rater) {
- return {
- rater: rater,
- score: 0,
- scoreDenominator: 0,
-
- errorSpans: 0,
- numWithErrors: 0,
-
- hotwFound: 0,
- hotwMissed: 0,
- timeSpentMS: 0,
- };
-}
-
-/**
- * Creates the key for a score weighted component or slices.
- * @param {string} name
- * @param {boolean=} isSlice
- * @return {string}
- */
-function mqmScoreKey(name, isSlice = false) {
- return (isSlice ? MQM_SCORE_SLICE_PREFIX : MQM_SCORE_WEIGHTED_PREFIX) + name;
-}
-
-/**
- * Strips the prefix from a key for a score component (previously assembled by
- * mqmScoreKey).
- * @param {string} key
- * @return {string}
- */
-function mqmScoreKeyToName(key) {
- if (key.startsWith(MQM_SCORE_WEIGHTED_PREFIX)) {
- return key.substr(MQM_SCORE_WEIGHTED_PREFIX.length);
- } else if (key.startsWith(MQM_SCORE_SLICE_PREFIX)) {
- return key.substr(MQM_SCORE_SLICE_PREFIX.length);
- }
- return key;
-}
-
-/**
- * Appends stats from delta into raterStats.
- * @param {!Object} raterStats
- * @param {!Object} delta
- */
-function mqmAddRaterStats(raterStats, delta) {
- raterStats.score += delta.score;
- for (sc of mqmWeights) {
- const key = mqmScoreKey(sc.name);
- if (delta[key]) {
- raterStats[key] = (raterStats[key] ?? 0) + delta[key];
- }
- }
- for (sc of mqmSlices) {
- const key = mqmScoreKey(sc.name, true);
- if (delta[key]) {
- raterStats[key] = (raterStats[key] ?? 0) + delta[key];
- }
- }
- raterStats.errorSpans += delta.errorSpans;
- raterStats.numWithErrors += delta.numWithErrors;
- raterStats.hotwFound += delta.hotwFound;
- raterStats.hotwMissed += delta.hotwMissed;
- raterStats.timeSpentMS += delta.timeSpentMS;
-}
-
-/**
- * Divides all metrics in raterStats by num.
- * @param {!Object} raterStats
- * @param {number} num
- */
-function mqmAvgRaterStats(raterStats, num) {
- if (!num) return;
- raterStats.score /= num;
- raterStats.timeSpentMS /= num;
- for (sc of mqmWeights) {
- const key = mqmScoreKey(sc.name);
- if (raterStats[key]) {
- raterStats[key] /= num;
- }
- }
- for (sc of mqmSlices) {
- const key = mqmScoreKey(sc.name, true);
- if (raterStats[key]) {
- raterStats[key] /= num;
- }
- }
-}
-
-/**
- * Aggregates segment stats. This returns an object that has aggregate MQM score
- * in the "score" field and these additional properties:
- * scoreDenominator
- * numSegments
- * numSrcChars
- * numRatings
- * metrics
- * metric-[index in mqmMetrics]
- * (repeated from metrics[...].score, as a convenient sorting key)
- * timeSpentMS
- * @param {!Array} segs
- * @return {!Object}
- */
-function mqmAggregateSegStats(segs) {
- const aggregates = mqmInitRaterStats('');
- aggregates.metrics = {};
- if (!segs || !segs.length) {
- aggregates.score = 0;
- aggregates.scoreDenominator = 0;
- aggregates.numSegments = 0;
- aggregates.numSrcChars = 0;
- aggregates.numRatings = 0;
- aggregates.timeSpentMS = 0;
- return aggregates;
- }
- let totalSrcLen = 0;
- /**
- * numSegRatings counts each (seg, rater) combination, where the rater rated
- * that segment, once.
- * numRatedSegs counts segments that have been rated by at least one rater.
- */
- let numSegRatings = 0;
- let numRatedSegs = 0;
- for (let segStats of segs) {
- const allRaterStats = mqmInitRaterStats('');
- for (let r of segStats) {
- mqmAddRaterStats(allRaterStats, r);
- }
- if (segStats.length > 0) {
- mqmAvgRaterStats(allRaterStats, segStats.length);
- numSegRatings += segStats.length;
- numRatedSegs++;
- totalSrcLen += segStats.srcLen;
- mqmAddRaterStats(aggregates, allRaterStats);
- }
- if (segStats.hasOwnProperty('metrics')) {
- for (let metric in segStats.metrics) {
- if (metric == 'MQM') {
- /**
- * Ignore any MQM values that may be present in the segment metadata
- * as we compute them from the annotations.
- */
- continue;
- }
- if (!aggregates.metrics.hasOwnProperty(metric)) {
- aggregates.metrics[metric] = {
- score: 0,
- scoreDenominator: 0,
- numSegments: 0,
- numSrcChars: 0,
- };
- }
- }
- }
- }
-
- aggregates.numSegments = numRatedSegs;
- aggregates.numSrcChars = totalSrcLen;
- aggregates.scoreDenominator =
- mqmCharScoring ? (aggregates.numSrcChars / 100) : aggregates.numSegments;
- mqmAvgRaterStats(aggregates, aggregates.scoreDenominator);
- aggregates.numRatings = numSegRatings;
-
- for (let metric in aggregates.metrics) {
- const metricStats = aggregates.metrics[metric];
- metricStats.numSegments = 0;
- metricStats.numSrcChars = 0;
- metricStats.score = 0;
- for (let segStats of segs) {
- if (!segStats.hasOwnProperty('metrics') ||
- !segStats.metrics.hasOwnProperty(metric)) {
- continue;
- }
- metricStats.numSegments++;
- metricStats.numSrcChars += segStats.srcLen;
- metricStats.score += segStats.metrics[metric];
- }
- metricStats.scoreDenominator =
- mqmCharScoring ? (metricStats.numSrcChars / 100) :
- metricStats.numSegments;
- if (metricStats.scoreDenominator > 0) {
- metricStats.score /= metricStats.scoreDenominator;
- }
- }
- /** Copy MQM score into aggregate.metrics['MQM'] */
- if (aggregates.numRatings > 0) {
- aggregates.metrics['MQM'] = {
- score: aggregates.score,
- scoreDenominator: aggregates.scoreDenominator,
- numSegments: aggregates.numSegments,
- numSrcChars: aggregates.numSrcChars,
- numRatings: aggregates.numRatings,
- };
- }
- for (let metric in aggregates.metrics) {
- const metricStats = aggregates.metrics[metric];
- const metricIndex = mqmMetricsInfo[metric].index;
- aggregates['metric-' + metricIndex] = metricStats.score;
- }
- return aggregates;
-}
-
-/**
- * This resets the significance tests data and terminates the active sigtests
- * computation Worker if it exists.
- */
-function mqmResetSigtests() {
- mqmSigtestsMsg.innerHTML = '';
- mqmSigtestsData.metricData = {};
- if (mqmSigtestsWorker) {
- mqmSigtestsWorker.terminate();
- }
- mqmSigtestsWorker = null;
-}
-
-/**
- * This prepares significance tests data, setting various fields in
- * mqmSigtestsData.
- * @param {!Object} mqmStatsBySysAggregates
- */
-function mqmPrepareSigtests(mqmStatsBySysAggregates) {
- /**
- * Each segment is uniquely determined by the (doc, docSegId) pair. We use
- * `pairToPos` to track which pair goes to which position in the aligned
- * segScoresBySystem[system] array.
- */
- const pairToPos = {};
- let maxPos = 0;
- for (const doc of mqmDataIter.docs) {
- pairToPos[doc] = {};
- for (const docSegId of mqmDataIter.docSegs[doc]) {
- pairToPos[doc][docSegId] = maxPos;
- maxPos += 1;
- }
- }
- const elt = document.getElementById('mqm-sigtests-num-trials');
- mqmSigtestsData.numTrials = parseInt(elt.value);
- mqmSigtestsData.metricData = {};
-
- const systems = Object.keys(mqmStatsBySysAggregates);
- systems.splice(systems.indexOf(MQM_TOTAL), 1);
-
- for (let m of mqmMetricsVisible) {
- const metricKey = 'metric-' + m;
- const metric = mqmMetrics[m];
- const metricInfo = mqmMetricsInfo[metric];
- const data = new MQMMetricSigtestsData();
- mqmSigtestsData.metricData[metric] = data;
- data.systems = systems.slice();
- data.lowerBetter = metricInfo.lowerBetter || false;
- const signReverser = metricInfo.lowerBetter ? 1.0 : -1.0;
- data.systems.sort(
- (s1, s2) => signReverser * (
- (mqmStatsBySysAggregates[s1][metricKey] ?? 0) -
- (mqmStatsBySysAggregates[s2][metricKey] ?? 0)));
- for (const system of data.systems) {
- data.scoresBySystem[system] =
- mqmStatsBySysAggregates[system].metrics[metric] ??
- {score: 0, scoreDenominator: 0};
- }
- segScores = data.segScoresBySystem;
- for (const system of data.systems) {
- /**
- * For each system, we first compute the mapping from position to score.
- * Any missing key correponds to one missing segment for this system.
- */
- const posToScore = {};
- for (const doc of Object.keys(mqmStats[system])) {
- for (const docSegId of Object.keys(mqmStats[system][doc])) {
- const pos = pairToPos[doc][docSegId];
- const segs = mqmStats[system][doc][docSegId];
- /** Note the extra "[]". */
- const aggregate = mqmAggregateSegStats([segs]);
- const metricStats = aggregate.metrics[metric] ?? null;
- if (metricStats && metricStats.scoreDenominator > 0) {
- posToScore[pos] = metricStats.score;
- }
- }
- }
- /** Now we can compute "segScores". */
- segScores[system] = [];
- for (let pos = 0; pos < maxPos; pos++) {
- if (posToScore.hasOwnProperty(pos)) {
- segScores[system].push(posToScore[pos]);
- } else {
- /** This system is missing this specific segment. */
- segScores[system].push(null);
- }
- }
- }
-
- /** Compute common positions for each system pair in `commonPos`. */
- const commonPos = data.commonPosBySystemPair;
- for (const [idx, baseline] of data.systems.entries()) {
- if (!commonPos.hasOwnProperty(baseline)) {
- commonPos[baseline] = {};
- }
- /** We only need the upper triangle in the significance test table. */
- for (const system of data.systems.slice(idx + 1)) {
- if (!commonPos[baseline].hasOwnProperty(system)) {
- commonPos[baseline][system] = [];
- }
- for (let pos = 0; pos < maxPos; pos++) {
- if ((segScores[system][pos] != null) &&
- (segScores[baseline][pos] != null)) {
- commonPos[baseline][system].push(pos);
- }
- }
- }
- }
-
- /** Create pValues matrix, to be populated with updates from the Worker. */
- const numSystems = data.systems.length;
- data.pValues = Array(numSystems);
- for (let row = 0; row < numSystems; row++) {
- data.pValues[row] = Array(numSystems);
- for (let col = 0; col < numSystems; col++) {
- data.pValues[row][col] = NaN;
- }
- }
- }
-}
-
-/**
- * In the significance tests table, draw a solid line under every prefix of
- * systems that is significantly better than all subsequent systems. Draw a
- * dotted line to separate clusters within which no system is significantly
- * better than any other.
- * @param {string} metric
- */
-function mqmClusterSigtests(metric) {
- const m = mqmMetricsInfo[metric].index;
- const data = mqmSigtestsData.metricData[metric];
- const numSystems = data.systems.length;
- const systemBetterThanAllAfter = Array(numSystems);
- for (let row = 0; row < numSystems; row++) {
- systemBetterThanAllAfter[row] = numSystems - 1;
- for (let col = numSystems - 1; col > row; col--) {
- const pValue = data.pValues[row][col];
- if (isNaN(pValue) || pValue >= MQM_PVALUE_THRESHOLD) {
- break;
- }
- systemBetterThanAllAfter[row] = col - 1;
- }
- }
- let maxBetterThanAllAfter = 0; /** Max over rows 0..row */
- let dottedClusterStart = 0;
- for (let row = 0; row < numSystems - 1; row++) {
- const tr = document.getElementById('mqm-sigtests-' + m + '-row-' + row);
- maxBetterThanAllAfter = Math.max(maxBetterThanAllAfter,
- systemBetterThanAllAfter[row]);
- if (maxBetterThanAllAfter == row) {
- tr.className = 'mqm-bottomed-tr';
- dottedClusterStart = row + 1;
- continue;
- }
- /** Is no system in dottedClusterStart..row signif. better than row+1? */
- let noneSigBetter = true;
- for (let dottedClusterRow = dottedClusterStart;
- dottedClusterRow <= row; dottedClusterRow++) {
- const pValue = data.pValues[dottedClusterRow][row + 1];
- if (!isNaN(pValue) && pValue < MQM_PVALUE_THRESHOLD) {
- noneSigBetter = false;
- break;
- }
- }
- if (!noneSigBetter) {
- tr.className = 'mqm-dotted-bottomed-tr';
- dottedClusterStart = row + 1;
- }
- }
-}
-
-/**
- * This receives a computation update from the Sigtests Worker thread. The
- * update consists of one p-value for a metric, row, col, or marks the
- * computation for that metric as done, or marks all computations as finished.
- * @param {!Event} e
- */
-function mqmSigtestsUpdate(e) {
- const update = e.data;
- if (update.finished) {
- mqmResetSigtests();
- return;
- }
- const metric = update.metric;
- if (update.metricDone) {
- mqmClusterSigtests(metric);
- return;
- }
- const m = mqmMetricsInfo[metric].index;
- const span = document.getElementById(
- `mqm-sigtest-${m}-${update.row}-${update.col}`);
- span.innerText = update.pValue.toFixed(3);
- span.title = `Based on ${update.numCommonSegs} common segments.`;
- if (update.pValue < MQM_PVALUE_THRESHOLD) {
- span.className = 'mqm-sigtest-significant';
- }
- mqmSigtestsData.metricData[metric].pValues[update.row][update.col] =
- update.pValue;
-}
-
-/**
- * Shows the table for significance tests.
- * @param {!Object} mqmStatsBySysAggregates
- */
-function mqmShowSigtests(mqmStatsBySysAggregates) {
- const div = document.getElementById('mqm-sigtests-tables');
- div.innerHTML = '';
- if (mqmCharScoring) {
- mqmSigtestsMsg.innerHTML = 'Not available for 100-source-chars scoring';
- return;
- }
- mqmPrepareSigtests(mqmStatsBySysAggregates);
- let firstTable = true;
- for (let m of mqmMetricsVisible) {
- const metric = mqmMetrics[m];
- const data = mqmSigtestsData.metricData[metric];
- const systems = data.systems;
- const scoresBySystem = data.scoresBySystem;
-
- /** Header. */
- let html = `
- ${firstTable ? '' : ' '}
-
-
-
-
System
-
${mqmMetrics[m]}
`;
- for (const system of systems) {
- const s = scoresBySystem[system];
- if (s.scoreDenominator == 0) {
- continue;
- }
- html += `
${system}
`;
- }
- html += `
\n\n`;
-
- /** Show significance test p-value placeholders. */
- for (const [rowIdx, baseline] of systems.entries()) {
- /** Show metric score in the second column. */
- const s = scoresBySystem[baseline];
- if (s.scoreDenominator == 0) {
- continue;
- }
- const displayScore = mqmMetricDisplay(s.score, s.scoreDenominator);
- html += `
-
\n';
- mqmStatsTable.insertAdjacentHTML('beforeend', rowHTML);
-}
-
-/**
- * Shows the system x rater matrix of scores. The rows and columns are
- * ordered by total MQM score.
- */
-function mqmShowSystemRaterStats() {
- const table = document.getElementById('mqm-system-x-rater');
-
- const systems = Object.keys(mqmStats);
- const systemAggregates = {};
- for (let sys of systems) {
- const segs = mqmGetSegStatsAsArray(mqmStats[sys]);
- systemAggregates[sys] = mqmAggregateSegStats(segs);
- }
-
- const SORT_FIELD = 'metric-0';
- systems.sort(
- (sys1, sys2) =>
- systemAggregates[sys1][SORT_FIELD] -
- systemAggregates[sys2][SORT_FIELD]);
-
- const raters = Object.keys(mqmStatsByRater);
- const raterAggregates = {};
- for (let rater of raters) {
- const segs = mqmGetSegStatsAsArray(mqmStatsByRater[rater][MQM_TOTAL]);
- raterAggregates[rater] = mqmAggregateSegStats(segs);
- }
- raters.sort(
- (rater1, rater2) =>
- raterAggregates[rater1][SORT_FIELD] -
- raterAggregates[rater2][SORT_FIELD]);
-
- let html = `
-
-
-
System
-
All raters
`;
- for (let rater of raters) {
- html += `
-
${rater}
`;
- }
- html += `
-
-
- `;
- /**
- * State for detecting "out-of-order" raters. We say a rater is out-of-order
- * if their rating for a system is oppositely related to the previous
- * system's rating, when compared with the aggregate over all raters.
- */
- let lastAllRaters = 0;
- const lastForRater = {};
- for (let rater of raters) {
- lastForRater[rater] = 0;
- }
- for (let sys of systems) {
- if (sys == MQM_TOTAL) {
- continue;
- }
- const allRatersScore = systemAggregates[sys].score;
- const allRatersScoreDisplay = mqmMetricDisplay(
- allRatersScore, systemAggregates[sys].numRatings);
- html += `
-
';
- /**
- * Ensure that the next score for this rater is marked as out-of-order
- * or not in some reasonable manner:
- */
- lastForRater[rater] = allRatersScore;
- }
- }
- lastAllRaters = allRatersScore;
- html += `
-
`;
- }
- html += `
- `;
- table.innerHTML = html;
-}
-
-/**
- * Sorter function where a & b are both a [doc, seg] pair. The seg part can
- * be a number (0,1,2,3,etc.) or a string, so we have to be careful to ensure
- * transitivity. We do that by assuming that the numbers are not too big, so
- * padding them with up to 10 zeros should be good enough.
- * @param {!Array} a
- * @param {!Array} b
- * @return {number} Comparison for sorting a & b.
- */
-function mqmDocSegsSorter(a, b) {
- if (a[0] < b[0]) return -1;
- if (a[0] > b[0]) return 1;
- seg1 = mqmCmpDocSegId(a[1]);
- seg2 = mqmCmpDocSegId(b[1]);
- if (seg1 < seg2) return -1;
- if (seg1 > seg2) return 1;
- return 0;
-}
-
-/**
- * From a stats object that's keyed on doc and then on segs, extracts all
- * [doc, seg] pairs into an array, sorts the array, and returns it.
- * @param {!Object} stats
- * @return {!Array}
- */
-function mqmGetDocSegs(stats) {
- const segs = [];
- for (let doc in stats) {
- const docstats = stats[doc];
- for (let docSeg in docstats) {
- segs.push([doc, docSeg]);
- }
- }
- return segs.sort(mqmDocSegsSorter);
-}
-
-/**
- * Makes a convenient key that captures a doc name and a docSegId.
- * @param {string} doc
- * @param {string|number} seg
- * @return {string}
- */
-function mqmDocSegKey(doc, seg) {
- return doc + ':' + seg;
-}
-
-/**
- * Helper class for building a segment scores histogram. Call addSegment() on it
- * multiple times to record segment scores. Then call display().
- *
- * The rendered histogram bins one special x-axis value differently, showing a
- * slim gray bar for that value. For difference histograms (hasDiffs=true), that
- * value is 0. Otherwise, it is for the perfect possible score (>= 1 for
- * automated metrics and 0 for MQM).
- *
- * @param {number} m The index of the metric in mqmMetrics.
- * @param {string} sys The name of the system.
- * @param {string} color The color to use for system.
- * @param {string=} sysCmp if the histogram is for diffs, then the name of
- * the system being compared against.
- * @param {string=} colorCmp if the histogram is for diffs, then the color of
- * the system being compared against.
- */
-function MQMHistBuilder(m, sys, color, sysCmp='', colorCmp='') {
- this.metricIndex = m;
- this.sys = sys;
- this.sysCmp = sysCmp;
- this.hasDiffs = sysCmp ? true : false;
- this.metric = mqmMetrics[m];
- this.color = color;
- this.colorCmp = colorCmp;
-
- const metricInfo = mqmMetricsInfo[this.metric];
- this.lowerBetter = metricInfo.lowerBetter || false;
-
- /**
- * Is there a dedicated bin for value == 0?
- */
- this.hasZeroBin = this.hasDiffs ? true : (this.lowerBetter ? true : false);
-
- /** @const {number} Width of a histogram bin, in score units */
- this.BIN_WIDTH = (this.metric == 'MQM') ? 0.5 : 0.05;
- this.BIN_PRECISION = (this.metric == 'MQM') ? 1 : 2;
-
- /** @const {number} Width of a histogram bin, in pixels */
- this.BIN_WIDTH_PIXELS = 10 + (this.BIN_PRECISION * 3);
-
- this.PIXELS_PER_UNIT = this.BIN_WIDTH_PIXELS / this.BIN_WIDTH;
-
- /** @const {number} Width of the special "zero" bin, in pixels */
- this.ZERO_BIN_WIDTH_PIXELS = 6;
-
- this.LOG_MULTIPLIER = 1.0 / Math.LN2;
- this.LOG_UNIT_HEIGHT_PIXELS = 25;
- this.TOP_OFFSET_PIXELS = this.hasDiffs ? 49 : 19;
- this.BOTTOM_OFFSET_PIXELS = 50;
- this.X_OFFSET_PIXELS = 50;
-
- this.COLOR_ZERO = 'rgba(211,211,211,0.5)';
- this.COLOR_OUTLINE = 'black';
- this.COLOR_LEGEND = 'black';
- this.COLOR_LABELS = 'black';
- this.COLOR_LINES = 'lightgray';
-
- /**
- * @const {!Object} Dict keyed by bin. Each bin has an array of doc-seg keys.
- * The only non-numeric key possibly present is 'zero' (when
- * this.hasZeroBin is true).
- */
- this.segsInBin = {};
-
- /**
- * @const {number} The largest bin visible on the X-axis.
- */
- this.maxBin = 0;
- /**
- * @const {number} The smallest bin visible on the X-axis.
- */
- this.minBin = 0;
-
- /** {number} The largest count in bin (used to determine height of plot) */
- this.maxCount = 8;
-
- this.totalCount = 0;
- this.sys1BetterCount = 0;
- this.sys2BetterCount = 0;
-}
-
-/**
- * Returns the bin for a particular value. We return the left end-point (except
- * for the special 'zero' bin).
- * @param {number} value
- * @return {string}
- */
-MQMHistBuilder.prototype.binOf = function(value) {
- if (this.hasZeroBin && value == 0) {
- return 'zero';
- }
- const absValue = Math.abs(value);
- const absBin = Math.floor(absValue / this.BIN_WIDTH) * this.BIN_WIDTH;
- const leftVal = (value < 0) ? (0 - absBin - this.BIN_WIDTH) : absBin;
- return leftVal.toFixed(this.BIN_PRECISION);
-};
-
-/**
- * Adds a segment to the histogram, updating the appropriate bin.
- * @param {string} doc
- * @param {string|number} docSegId
- * @param {number} value The score for the first system
- */
-MQMHistBuilder.prototype.addSegment = function(doc, docSegId, value) {
- const bin = this.binOf(value);
- const numericBin = (bin == 'zero') ? 0 : parseFloat(bin);
- if (numericBin < this.minBin) this.minBin = numericBin;
- if (numericBin > this.maxBin) this.maxBin = numericBin;
- const docSegKey = mqmDocSegKey(doc, docSegId);
- if (!this.segsInBin.hasOwnProperty(bin)) {
- this.segsInBin[bin] = [];
- }
- this.segsInBin[bin].push(docSegKey);
- if (this.segsInBin[bin].length > this.maxCount) {
- this.maxCount = this.segsInBin[bin].length;
- }
- this.totalCount++;
- if (this.hasDiffs && bin != 'zero') {
- const firstLower = (numericBin < 0);
- const firstBetter = (firstLower && this.lowerBetter) ||
- (!firstLower && !this.lowerBetter);
- if (firstBetter) {
- this.sys1BetterCount++;
- } else {
- this.sys2BetterCount++;
- }
- }
-};
-
-/**
- * Creates and returns an SVG rect.
- * @param {number} x
- * @param {number} y
- * @param {number} w
- * @param {number} h
- * @param {string} color
- * @return {!Element}
- */
-MQMHistBuilder.prototype.getRect = function(x, y, w, h, color) {
- const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
- rect.setAttributeNS(null, 'x', x);
- rect.setAttributeNS(null, 'y', y + this.TOP_OFFSET_PIXELS);
- rect.setAttributeNS(null, 'width', w);
- rect.setAttributeNS(null, 'height', h);
- rect.style.fill = color;
- rect.style.stroke = this.COLOR_OUTLINE;
- return rect;
-};
-
-/**
- * Creates a histogram bar with a given description, makes it clickable to
- * constrain the view to the docsegs passed.
- * @param {!Element} plot
- * @param {number} x
- * @param {number} y
- * @param {number} w
- * @param {number} h
- * @param {string} color
- * @param {string} desc
- * @param {!Array} docsegs
- */
-MQMHistBuilder.prototype.makeHistBar = function(
- plot, x, y, w, h, color, desc, docsegs) {
- /**
- * Need to wrap the rect in a g (group) element to be able to show
- * the description when hovering ("title" attribute does not work with SVG
- * elements).
- */
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- g.setAttributeNS(null, 'class', 'mqm-sys-v-sys-hist');
- g.insertAdjacentHTML('beforeend',
- `Click to see examples of ${desc}`);
- const rect = this.getRect(x, y, w, h, color);
- g.appendChild(rect);
- const viewingConstraints = {};
- for (let ds of docsegs) {
- viewingConstraints[ds] = true;
- }
- viewingConstraints.description = desc;
- viewingConstraints.color = color;
- g.addEventListener('click', (e) => {
- mqmShow(viewingConstraints);
- });
- plot.appendChild(g);
-};
-
-/**
- * Creates a line on the plot.
- * @param {!Element} plot
- * @param {number} x1
- * @param {number} y1
- * @param {number} x2
- * @param {number} y2
- * @param {string} color
- */
-MQMHistBuilder.prototype.makeLine = function(
- plot, x1, y1, x2, y2, color) {
- const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
- line.setAttributeNS(null, 'x1', x1);
- line.setAttributeNS(null, 'y1', y1 + this.TOP_OFFSET_PIXELS);
- line.setAttributeNS(null, 'x2', x2);
- line.setAttributeNS(null, 'y2', y2 + this.TOP_OFFSET_PIXELS);
- line.style.stroke = color;
- plot.appendChild(line);
-};
-
-/**
- * Writes some text on the plot.
- * @param {!Element} plot
- * @param {number} x
- * @param {number} y
- * @param {string} s
- * @param {string} color
- */
-MQMHistBuilder.prototype.makeText = function(plot, x, y, s, color) {
- const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
- text.setAttributeNS(null, 'x', x);
- text.setAttributeNS(null, 'y', y + this.TOP_OFFSET_PIXELS);
- text.innerHTML = s;
- text.style.fill = color;
- text.style.fontSize = '10px';
- plot.appendChild(text);
-};
-
-/**
- * Returns height in pixels for a histogram bar with the given count.
- * @param {number} count
- * @return {number}
- */
-MQMHistBuilder.prototype.heightInPixels = function(count) {
- if (count == 0) return 0;
- return this.LOG_UNIT_HEIGHT_PIXELS *
- ((Math.log(count) * this.LOG_MULTIPLIER) + 1);
-};
-
-/**
- * Returns the color to use for the bin's histogram rectangle.
- * @param {string} bin
- * @param {number} numericBin
- * @return {string}
- */
-MQMHistBuilder.prototype.binColor = function(bin, numericBin) {
- if (bin == 'zero') {
- return this.COLOR_ZERO;
- }
- if (!this.hasDiffs) {
- return this.color;
- } else {
- const firstLower = (numericBin < 0);
- const firstBetter = (firstLower && this.lowerBetter) ||
- (!firstLower && !this.lowerBetter);
- return firstBetter ? this.color : this.colorCmp;
- }
-};
-
-/**
- * Returns a description of the bin.
- * @param {string} bin
- * @param {number} numericBin
- * @param {number} count
- * @return {string}
- */
-MQMHistBuilder.prototype.binDesc = function(bin, numericBin, count) {
- if (!this.hasDiffs) {
- if (bin == 'zero') {
- return '' + count + ' segment(s) where ' + this.sys +
- ' has ' + this.metric + ' score exactly equal to 0';
- }
- const binLeft = numericBin;
- const binRight = numericBin + this.BIN_WIDTH;
- let leftParen = (numericBin < 0) ? '(' :
- ((numericBin == 0 && this.hasZeroBin) ? '(' : '[');
- let rightParen = (numericBin < 0) ?
- ((binRight == 0 && this.hasZeroBin) ? ')' : ']') : ')';
- return '' + count + ' segment(s) where ' + this.sys + ' has ' +
- this.metric + ' score in ' + 'range ' + leftParen +
- this.binDisplay(binLeft) +
- ',' + this.binDisplay(binRight) + rightParen;
- } else {
- if (bin == 'zero') {
- return '' + count + ' segment(s) where ' + this.sys + ' and ' +
- this.sysCmp + ' have identical ' + this.metric + ' scores';
- }
- const firstLower = (numericBin < 0);
- const firstBetter = (firstLower && this.lowerBetter) ||
- (!firstLower && !this.lowerBetter);
- const betterSys = firstBetter ? this.sys : this.sysCmp;
- const worseSys = firstBetter ? this.sysCmp : this.sys;
- const binLeft = numericBin;
- const binRight = numericBin + this.BIN_WIDTH;
- const absBinLeft = (numericBin < 0) ? (0 - binRight) : binLeft;
- const absBinRight = absBinLeft + this.BIN_WIDTH;
- const firstParen = (absBinLeft == 0 && this.hasZeroBin) ? '(' : '[';
- return '' + count + ' segment(s) where ' + betterSys + ' is better than ' +
- worseSys + ' with ' + this.metric + ' score diff in range ' +
- firstParen + this.binDisplay(absBinLeft) + ',' +
- this.binDisplay(absBinRight) + ')';
- }
-};
-
-/**
- * Returns the x coordinate in pixels for a particular metric value.
- * @param {number} value
- * @return {number}
- */
-MQMHistBuilder.prototype.xPixels = function(value) {
- return this.X_OFFSET_PIXELS + ((value - this.minBin) * this.PIXELS_PER_UNIT);
-};
-
-/**
- * Returns a string suitable to display, for a floating-point number. Strips
- * trailing zeros and then a trailing decimal point.
- * @param {number} value
- * @return {string}
- */
-MQMHistBuilder.prototype.binDisplay = function(value) {
- return value.toFixed(
- this.BIN_PRECISION).replace(/0+$/, '').replace(/\.$/, '');
-};
-
-/**
- * Displays the histogram using the data collected through prior addSegment()
- * calls.
- * @param {!Element} plot
- */
-MQMHistBuilder.prototype.display = function(plot) {
- /** Create some buffer space above the plot. */
- this.maxCount += 10;
-
- const binKeys = Object.keys(this.segsInBin);
- /** Sort so that 'zero' bin is drawn at the end. */
- binKeys.sort((a, b) => {
- let a2 = (a == 'zero') ? Number.MAX_VALUE : a;
- let b2 = (b == 'zero') ? Number.MAX_VALUE : b;
- return a2 - b2;
- });
- const plotWidth = Math.max(
- 400, (2 * this.X_OFFSET_PIXELS) +
- ((this.maxBin - this.minBin) * this.PIXELS_PER_UNIT));
- const plotHeight = this.heightInPixels(this.maxCount);
- const svgWidth = plotWidth;
- const svgHeight = plotHeight +
- (this.TOP_OFFSET_PIXELS + this.BOTTOM_OFFSET_PIXELS);
- plot.innerHTML = '';
- plot.setAttributeNS(null, 'viewBox', `0 0 ${svgWidth} ${svgHeight}`);
- plot.setAttributeNS(null, 'width', svgWidth);
- plot.setAttributeNS(null, 'height', svgHeight);
-
- /* y axis labels */
- this.makeLine(plot, 0, plotHeight, plotWidth, plotHeight, this.COLOR_LINES);
- this.makeText(plot, 5, plotHeight - 2, '0', this.COLOR_LABELS);
- for (let l = 1; l <= this.maxCount; l *= 2) {
- const h = this.heightInPixels(l);
- this.makeLine(plot, 0, plotHeight - h, plotWidth, plotHeight - h,
- this.COLOR_LINES);
- this.makeText(plot, 5, plotHeight - h - 2, '' + l, this.COLOR_LABELS);
- }
-
- if (this.hasDiffs) {
- /* legend, shown in the area above the plot */
- legends = [
- {
- color: this.color,
- desc: this.sys1BetterCount + ' better segments for ' + this.sys,
- },
- {
- color: this.colorCmp,
- desc: this.sys2BetterCount + ' better segments for ' + this.sysCmp,
- },
- ];
- for (let s = 0; s < legends.length; s++) {
- const legend = legends[s];
- const y = -30 + (s * (this.BIN_WIDTH_PIXELS + 10));
- const x = 25;
- plot.appendChild(this.getRect(
- x, y, this.BIN_WIDTH_PIXELS, this.BIN_WIDTH_PIXELS, legend.color));
- this.makeText(plot, x + this.BIN_WIDTH_PIXELS + 5, y + 10,
- legend.desc, this.COLOR_LEGEND);
- }
- }
-
- for (let bin of binKeys) {
- const segs = this.segsInBin[bin];
- if (segs.length == 0) continue;
- const numericBin = (bin == 'zero') ? 0 : parseFloat(bin);
- let x = this.xPixels(numericBin);
- const binWidth = (bin == 'zero') ? this.ZERO_BIN_WIDTH_PIXELS :
- this.BIN_WIDTH_PIXELS;
- if (bin == 'zero') {
- x -= (binWidth / 2.0);
- }
- const color = this.binColor(bin, numericBin);
- const desc = this.binDesc(bin, numericBin, segs.length);
- const h = this.heightInPixels(segs.length);
- this.makeHistBar(
- plot, x, plotHeight - h, binWidth, h,
- color, desc, segs);
- }
-
- /** Draw x-axis labels */
- const maxV = Math.max(Math.abs(this.minBin), Math.abs(this.maxBin));
- const step = 2 * this.BIN_WIDTH;
- for (let v = 0; v <= maxV + this.BIN_WIDTH; v += step) {
- if (v >= 0 && v <= this.maxBin + this.BIN_WIDTH) {
- const vDisp = this.binDisplay(v);
- const x = this.xPixels(v);
- const xDelta = 3 * vDisp.length;
- this.makeLine(plot, x, plotHeight, x, plotHeight + 8, this.COLOR_LINES);
- this.makeText(plot, x - xDelta, plotHeight + 20,
- vDisp, this.COLOR_LABELS);
- }
- const negV = 0 - v;
- if (v == 0 || negV < this.minBin) {
- continue;
- }
- const negVDisp = this.binDisplay(negV);
- const x = this.xPixels(negV);
- const xDelta = 3 * (negVDisp.length + 1);
- this.makeLine(plot, x, plotHeight, x, plotHeight + 8, this.COLOR_LINES);
- this.makeText(plot, x - xDelta, plotHeight + 20,
- negVDisp, this.COLOR_LABELS);
- }
- /* X-axis name */
- this.makeText(plot, this.X_OFFSET_PIXELS, plotHeight + 40,
- (this.hasDiffs ? this.metric + ' score differences' :
- this.sys + ': ' + this.totalCount + ' segments with ' +
- this.metric + ' scores'),
- this.COLOR_LEGEND);
-};
-
-/**
- * Creates the "system vs system" plots comparing two systems for all
- * available metrics. This sets up the menus for selecting the systems,
- * creates skeletal tables, and then calls mqmShowSysVSys() to populate the
- * tables.
- */
-function mqmCreateSysVSysTables() {
- const div = document.getElementById('mqm-sys-v-sys');
- div.innerHTML = `
-
-
- segment(s).
-
- segment(s)
- ( common).
- The Y-axis uses a log scale.
-
- `;
- for (let m of mqmMetricsVisible) {
- const metric = mqmMetrics[m];
- const html = `
-
- ${metric}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
`;
- div.insertAdjacentHTML('beforeend', html);
- }
-
- /** Populate menu choices. */
- const selectSys1 = document.getElementById('mqm-sys-v-sys-1');
- const selectSys2 = document.getElementById('mqm-sys-v-sys-2');
- const systems = Object.keys(mqmStats);
- /**
- * If possible, use the previously set values.
- */
- if (mqmSysVSys1 && !mqmStats.hasOwnProperty(mqmSysVSys1)) {
- mqmSysVSys1 = '';
- }
- if (mqmSysVSys2 && !mqmStats.hasOwnProperty(mqmSysVSys2)) {
- mqmSysVSys2 = '';
- }
- if (systems.length == 1) {
- mqmSysVSys1 = systems[0];
- mqmSysVSys2 = systems[0];
- }
- for (let system of systems) {
- if (system == MQM_TOTAL) {
- continue;
- }
- if (!mqmSysVSys1) {
- mqmSysVSys1 = system;
- }
- if (!mqmSysVSys2 && system != mqmSysVSys1) {
- mqmSysVSys2 = system;
- }
- const option1 = document.createElement('option');
- option1.value = system;
- option1.innerHTML = system;
- if (system == mqmSysVSys1) {
- option1.selected = true;
- }
- selectSys1.insertAdjacentElement('beforeend', option1);
- const option2 = document.createElement('option');
- option2.value = system;
- option2.innerHTML = system;
- if (system == mqmSysVSys2) {
- option2.selected = true;
- }
- selectSys2.insertAdjacentElement('beforeend', option2);
- }
- mqmShowSysVSys();
-}
-
-/**
- * Shows the system v system histograms of segment score differences.
- */
-function mqmShowSysVSys() {
- const selectSys1 = document.getElementById('mqm-sys-v-sys-1');
- const selectSys2 = document.getElementById('mqm-sys-v-sys-2');
- mqmSysVSys1 = selectSys1.value;
- mqmSysVSys2 = selectSys2.value;
- const docsegs1 = mqmGetDocSegs(mqmStats[mqmSysVSys1] || {});
- const docsegs2 = mqmGetDocSegs(mqmStats[mqmSysVSys2] || {});
- /**
- * Find common segments.
- */
- let i1 = 0;
- let i2 = 0;
- const docsegs12 = [];
- while (i1 < docsegs1.length && i2 < docsegs2.length) {
- const ds1 = docsegs1[i1];
- const ds2 = docsegs2[i2];
- const sort = mqmDocSegsSorter(ds1, ds2);
- if (sort < 0) {
- i1++;
- } else if (sort > 0) {
- i2++;
- } else {
- docsegs12.push(ds1);
- i1++;
- i2++;
- }
- }
- document.getElementById('mqm-sys-v-sys-xsegs').innerHTML = docsegs12.length;
- document.getElementById('mqm-sys-v-sys-1-segs').innerHTML = docsegs1.length;
- document.getElementById('mqm-sys-v-sys-2-segs').innerHTML = docsegs2.length;
-
- const sameSys = mqmSysVSys1 == mqmSysVSys2;
-
- for (let m of mqmMetricsVisible) {
- const metricKey = 'metric-' + m;
- /**
- * We draw up to 3 plots for a metric: system-1, system-2, and their diff.
- */
- const hists = [
- {
- docsegs: docsegs1,
- hide: !mqmSysVSys1,
- sys: mqmSysVSys1,
- color: 'lightgreen',
- sysCmp: '',
- colorCmp: '',
- id: 'mqm-sys1-plot-' + m,
- },
- {
- docsegs: docsegs2,
- hide: sameSys,
- sys: mqmSysVSys2,
- color: 'lightblue',
- sysCmp: '',
- colorCmp: '',
- id: 'mqm-sys2-plot-' + m,
- },
- {
- docsegs: docsegs12,
- hide: sameSys,
- sys: mqmSysVSys1,
- color: 'lightgreen',
- sysCmp: mqmSysVSys2,
- colorCmp: 'lightblue',
- id: 'mqm-sys-v-sys-plot-' + m,
- },
- ];
- for (let hist of hists) {
- const histElt = document.getElementById(hist.id);
- histElt.style.display = hist.hide ? 'none' : '';
- if (hist.hide) {
- continue;
- }
- const histBuilder = new MQMHistBuilder(m, hist.sys, hist.color,
- hist.sysCmp, hist.colorCmp);
- for (let i = 0; i < hist.docsegs.length; i++) {
- const doc = hist.docsegs[i][0];
- const docSegId = hist.docsegs[i][1];
- const aggregate1 = mqmAggregateSegStats(
- [mqmStats[hist.sys][doc][docSegId]]);
- if (!aggregate1.hasOwnProperty(metricKey)) {
- continue;
- }
- let score = aggregate1[metricKey];
- if (hist.sysCmp) {
- const aggregate2 = mqmAggregateSegStats(
- [mqmStats[hist.sysCmp][doc][docSegId]]);
- if (!aggregate2.hasOwnProperty(metricKey)) {
- continue;
- }
- score -= aggregate2[metricKey];
- }
- histBuilder.addSegment(doc, docSegId, score);
- }
- histBuilder.display(histElt);
- }
- }
-}
-
-/**
- * Shows details of severity- and category-wise scores (from the
- * mqmSevCatStats object) in the categories table.
- */
-function mqmShowSevCatStats() {
- const stats = mqmSevCatStats;
- const systems = {};
- for (let severity in stats) {
- for (let category in stats[severity]) {
- for (let system in stats[severity][category]) {
- if (!systems[system]) systems[system] = 0;
- systems[system] += stats[severity][category][system];
- }
- }
- }
- const systemsList = Object.keys(systems);
- const colspan = systemsList.length || 1;
- const th = document.getElementById('mqm-sevcat-stats-th');
- th.colSpan = colspan;
-
- systemsList.sort((sys1, sys2) => systems[sys2] - systems[sys1]);
-
- let rowHTML = '
';
- for (let system of systemsList) {
- rowHTML += `
- Systems above any solid line are significantly better than
- those below. Dotted lines identify clusters within which no
- system is significantly better than any other system.
-
-
- Number of trials for paired one-sided approximate randomization:
-
-
-
-
-
-
-
-
-
-
- System × Rater scores
-
-
-
-
-
-
-
-
-
-
-
- System segment scores and comparative histograms
-
-
-
- Rater timeline for
-
- (limited to ${MQM_RATER_TIMELINE_LIMIT} events)
-
-
-
-
-
Timestamp
-
Event
-
Document
-
System
-
Segment
-
Side
-
Sentence
-
Visible
-
-
-
-
-
-
-
-
-
-
-
- Filters
- (0 of
- 0 rows pass filters)
-
-
-
-
- Select only the segments for which all or no systems have scores
-
-
-
-
-
- You can click on any System/Doc/ID/Rater/Category/Severity (or pick
- from the drop-down list under the column name) to set its column
- filter to that specific value.
-
-
- You can provide column filter regular expressions for filtering
- one or more columns, in the input fields provided under the column
- names.
-
-
- You can create sophisticated filters (involving multiple columns, for
- example) using a JavaScript filter expression:
-
-
-
-
-
-
This allows you to filter using any expression
- involving the columns. It can use the following
- variables: system, doc, globalSegId,
- docSegId, rater, category, severity,
- source, target, metadata.
-
-
- Filter expressions also have access to three aggregated objects
- named aggrDocSegSys (which is simply an alias for
- metadata.segment), aggrDocSeg, and aggrDoc.
- The aggrDocSegSys dict also contains aggrDocSeg (with the key
- "aggrDocSeg"), which in turn similarly contains aggrDoc.
-
-
- The aggregated variable named aggrDocSeg is an object with
- the following properties:
- aggrDocSeg.catsBySystem,
- aggrDocSeg.catsByRater,
- aggrDocSeg.sevsBySystem,
- aggrDocSeg.sevsByRater,
- aggrDocSeg.sevcatsBySystem,
- aggrDocSeg.sevcatsByRater.
- Each of these properties is an object
- keyed by system or rater, with the values being arrays of strings.
- The "sevcats*" values look like "Minor/Fluency/Punctuation" or
- are just the same as severities if categories are empty. This
- segment-level aggregation allows you to select specific segments
- rather than just specific error ratings.
-
-
- System-wise metrics, including MQM, are also available in
- aggrDocSeg.metrics, which is an object keyed by the metric
- name and then by system name.
-
-
- The aggregated variable named aggrDoc is an object
- with the following properties that are aggregates over all
- the systems:
- doc, thumbsUpCount, thumbsDownCount.
-
-
- Log metadata for row to JavaScript console
- (open with Ctrl-Shift-I):
-
- (useful for finding available fields for filter expressions).
-
- You can add segment-level filtering clauses (AND/OR) using this
- helper (which uses convenient shortcut functions
- mqmIncl()/mqmExcl() for checking that a rating exists and
- has/does-not-have a value):
-