tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit b93e40e1e23baa885fd419a45d574efe80c87547
parent 45f78eb8a4ce94ff423616cb3f0b286e16391541
Author: Charlie Humphreys <chumphreys@mozilla.com>
Date:   Thu, 16 Oct 2025 18:35:58 +0000

Bug 1992754: Add settings pane for about:glean metrics table and charts r=TravisLong,fluent-reviewers,toolkit-telemetry-reviewers,bolsson

Differential Revision: https://phabricator.services.mozilla.com/D267728

Diffstat:
Mtoolkit/content/aboutGlean.css | 51++++++++++++++++++++++++++++++++++++++++++++++++---
Mtoolkit/content/aboutGlean.html | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtoolkit/content/aboutGlean.js | 314++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtoolkit/content/tests/browser/browser_about_glean.js | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/locales/en-US/toolkit/about/aboutGlean.ftl | 30++++++++++++++++++++++++++++++
5 files changed, 510 insertions(+), 53 deletions(-)

diff --git a/toolkit/content/aboutGlean.css b/toolkit/content/aboutGlean.css @@ -72,13 +72,53 @@ body { overflow-y: scroll; } +#metrics-table-header { + display: flex; + justify-content: space-between; +} + +#metrics-table-settings:not([hidden="true"]) .content { + display: flex; + flex-wrap: wrap; +} + +#metrics-table-settings .content > * { + flex: 1 0 auto; +} + +@media (max-width: 1096px) { + #metrics-table-settings .content > * { + flex-basis: 100%; + } +} + +#metrics-table-settings, +#metrics-table-settings .box { + margin-bottom: 5px; + padding: 10px; + border: 1px solid var(--border-color-card); + border-radius: var(--border-radius-small); + background-color: var(--background-color-box); +} + +#metrics-table-settings h2, +#metrics-table-settings h3, +#metrics-table-settings h4 { + margin: 10px 0; +} + +#metrics-table-settings hr { + margin: 20px 0; +} + td[data-d3-cell="actions"] > div { display: flex; justify-content: center; align-items: center; } -td[data-d3-cell="value"] > pre { +td[data-d3-cell="value"] > pre, +#metrics-table-settings-timeline-example > pre { background-color: var(--background-color-box); padding: 5px; border-radius: var(--border-radius-small); @@ -86,13 +126,19 @@ td[data-d3-cell="value"] > pre { width: fit-content; } -td[data-d3-cell="value"] > svg { +td[data-d3-cell="value"] > svg, +.chart-display > svg { color: var(--text-color); border: 1px solid var(--border-color-card); border-radius: var(--border-radius-small); background-color: var(--background-color-box); } +.vertical-flex { + display: flex; + flex-direction: column; +} + .histogram .boxes rect, .timeline .events circle { fill: var(--color-accent-primary); @@ -118,7 +164,6 @@ td[data-d3-cell="value"] > svg { stroke: var(--text-color); } -svg.timeline, pre.withChart { max-width: 500px; overflow: scroll; diff --git a/toolkit/content/aboutGlean.html b/toolkit/content/aboutGlean.html @@ -135,7 +135,85 @@ </ul> </div> <div id="metrics-table" class="tab" hidden="true"> - <h2 data-l10n-id="about-glean-metrics-table-header"></h2> + <div id="metrics-table-header"> + <h2 data-l10n-id="about-glean-metrics-table-header"></h2> + <button name="metrics-table-settings-button" id="metrics-table-settings-button" data-l10n-id="about-glean-metrics-table-settings-button"></button> + </div> + <div id="metrics-table-settings" hidden="true"> + <h2 data-l10n-id="about-glean-metrics-table-settings-title"></h2> + <div class="content"> + <div> + <h3 data-l10n-id="about-glean-metrics-table-settings-category-general"></h3> + <input type="checkbox" name="settings-hide-empty-value-rows" id="settings-hide-empty-value-rows" data-form-control="hideEmptyValueRows"/> + <label for="settings-hide-empty-value-rows" data-l10n-id="about-glean-metrics-table-settings-hide-empty-value-rows"></label> + </div> + <div> + <h3 data-l10n-id="about-glean-metrics-table-settings-category-visualizations"></h3> + <div class="box" data-form-group="histograms"> + <h4 data-l10n-id="about-glean-metrics-table-settings-category-visualizations-histogram"></h4> + + <div class="vertical-flex"> + <div> + <label for="settings-histograms-box-padding" data-l10n-id="about-glean-metrics-table-settings-histograms-box-padding"></label> + <input name="settings-histograms-box-padding" id="settings-histograms-box-padding" type="number" data-form-control="boxPadding"/> + </div> + <div> + <label for="settings-histograms-left-padding" data-l10n-id="about-glean-metrics-table-settings-histograms-left-padding"></label> + <input name="settings-histograms-left-padding" id="settings-histograms-left-padding" type="number" data-form-control="leftPadding"/> + </div> + <div> + <label for="settings-histograms-chart-padding" data-l10n-id="about-glean-metrics-table-settings-histograms-chart-padding"></label> + <input name="settings-histograms-chart-padding" id="settings-histograms-chart-padding" type="number" data-form-control="chartPadding"/> + </div> + <div> + <label for="settings-histograms-scaled-max" data-l10n-id="about-glean-metrics-table-settings-histograms-scaled-max"></label> + <input name="settings-histograms-scaled-max" id="settings-histograms-scaled-max" type="number" data-form-control="scaledMax"/> + </div> + <div> + <label for="settings-histograms-chart-max" data-l10n-id="about-glean-metrics-table-settings-histograms-chart-max"></label> + <input name="settings-histograms-chart-max" id="settings-histograms-chart-max" type="number" data-form-control="chartMax"/> + </div> + </div> + + <h4 data-l10n-id="about-glean-metrics-table-settings-visualization-example"></h4> + <div id="metrics-table-settings-histogram-example" class="chart-display"></div> + </div> + <div class="box" data-form-group="timelines"> + <h4 data-l10n-id="about-glean-metrics-table-settings-category-visualizations-timeline"></h4> + + <div class="vertical-flex"> + <div> + <label for="settings-timelines-height" data-l10n-id="about-glean-metrics-table-settings-timelines-height"></label> + <input name="settings-timelines-height" id="settings-timelines-height" type="number" data-form-control="height"/> + </div> + <div> + <label for="settings-timelines-width" data-l10n-id="about-glean-metrics-table-settings-timelines-width"></label> + <input name="settings-timelines-width" id="settings-timelines-width" type="number" data-form-control="width"/> + </div> + <div> + <label for="settings-timelines-chart-padding" data-l10n-id="about-glean-metrics-table-settings-timelines-chart-padding"></label> + <input name="settings-timelines-chart-padding" id="settings-timelines-chart-padding" type="number" data-form-control="chartPadding"/> + </div> + <div> + <label for="settings-timelines-circle-radius" data-l10n-id="about-glean-metrics-table-settings-timelines-circle-radius"></label> + <input name="settings-timelines-circle-radius" id="settings-timelines-circle-radius" type="number" data-form-control="circleRadius"/> + </div> + <div> + <label for="settings-timelines-vertical-line-x-offset" data-l10n-id="about-glean-metrics-table-settings-timelines-vertical-line-x-offset"></label> + <input name="settings-timelines-vertical-line-x-offset" id="settings-timelines-vertical-line-x-offset" type="number" data-form-control="verticalLineXOffset"/> + </div> + <div> + <label for="settings-timelines-vertical-line-y-offset" data-l10n-id="about-glean-metrics-table-settings-timelines-vertical-line-y-offset"></label> + <input name="settings-timelines-vertical-line-y-offset" id="settings-timelines-vertical-line-y-offset" type="number" data-form-control="verticalLineYOffset"/> + </div> + </div> + + <h4 data-l10n-id="about-glean-metrics-table-settings-visualization-example"></h4> + <div id="metrics-table-settings-timeline-example" class="chart-display"></div> + </div> + </div> + </div> + </div> <div id="metrics-table-controls"> <div class="input"> <label for="filter-metrics" data-l10n-id="about-glean-label-for-filter-metrics"></label> diff --git a/toolkit/content/aboutGlean.js b/toolkit/content/aboutGlean.js @@ -207,7 +207,9 @@ function showTab(button) { ...metric, })) ); - updateFilteredMetricData(); + updateFilteredMetricData( + document.getElementById("filter-metrics").value.toLowerCase() + ); updateTable(); } } @@ -242,7 +244,7 @@ function handleRedesign() { document.getElementById("filter-metrics").addEventListener("input", e => { clearTimeout(inputTimeout); inputTimeout = setTimeout(() => { - updateFilteredMetricData(e.target.value ?? ""); + updateFilteredMetricData(e.target.value.toLowerCase() ?? ""); }, 200); }); @@ -324,6 +326,19 @@ function onLoad() { } handleRedesign(); }); + + document + .getElementById("metrics-table-settings-button") + .addEventListener("click", () => { + const settingsDiv = document.getElementById("metrics-table-settings"); + if (settingsDiv.getAttribute("hidden")) { + settingsDiv.removeAttribute("hidden"); + } else { + settingsDiv.setAttribute("hidden", true); + } + settingsChanged(); + }); + initMetricsSettings(); } /** @@ -381,15 +396,18 @@ function createOrUpdateHistogram(selection, datum) { } selection.select("p")?.remove(); + const chartSettings = { + boxPadding: 5, + chartMax: 150, + leftPadding: 20, + chartPadding: 50, + scaledMax: 110, + ...metricsTableSettings.histograms, + }; const max = values.map(d => d[1]).sort((a, b) => b - a)[0], keyMax = values.map(d => d[0]).sort((a, b) => b - a)[0], boxWidth = Math.max(`${Math.max(max, keyMax)}`.length * 10, 30), - boxPadding = 5, - chartMax = 150, - leftPadding = 20, - chartPadding = 50, - scaledMax = 110, - denom = max / scaledMax; + denom = max / chartSettings.scaledMax; let hist = selection.select(`svg[data-d3-datum='${datum.fullName}']`); if (hist.empty()) { @@ -399,9 +417,11 @@ function createOrUpdateHistogram(selection, datum) { .attr("data-d3-datum", datum.fullName) .attr( "width", - values.length * (boxWidth + boxPadding) + chartPadding + leftPadding + values.length * (boxWidth + chartSettings.boxPadding) + + chartSettings.chartPadding + + chartSettings.leftPadding ) - .attr("height", chartMax + chartPadding); + .attr("height", chartSettings.chartMax + chartSettings.chartPadding); } let boxesContainer = hist.select("g.boxes"); @@ -416,9 +436,13 @@ function createOrUpdateHistogram(selection, datum) { newBoxes.append("text").attr("data-d3-role", "y"); newBoxes.append("text").attr("data-d3-role", "x"); - const xFn = index => boxWidth * index + boxPadding * index + leftPadding; + const xFn = index => + boxWidth * index + + chartSettings.boxPadding * index + + chartSettings.leftPadding; const yFn = yv => - Math.abs(Math.max(yv / denom, 1) - scaledMax) + (chartMax - scaledMax); + Math.abs(Math.max(yv / denom, 1) - chartSettings.scaledMax) + + (chartSettings.chartMax - chartSettings.scaledMax); boxes .selectAll("rect") @@ -433,7 +457,7 @@ function createOrUpdateHistogram(selection, datum) { boxes .selectAll("text[data-d3-role=x]") .attr("x", d => xFn(d[2])) - .attr("y", chartMax + 20) + .attr("y", chartSettings.chartMax + 20) .text(d => d[0]); function focusStart(_) { @@ -471,40 +495,72 @@ function createOrUpdateEventChart(selection, datum) { } selection.select("p")?.remove(); - const height = 75, - width = 500, - max = values.map(d => d.timestamp).sort((a, b) => b - a)[0], - min = values.map(d => d.timestamp).sort((a, b) => a - b)[0], - chartPadding = 50, - circleRadius = 6; + const chartSettings = { + height: 75, + width: 500, + chartPadding: 50, + circleRadius: 6, + verticalLineXOffset: 10, + verticalLineYOffset: 10, + ...metricsTableSettings.timelines, + }; + const max = values.map(d => d.timestamp).sort((a, b) => b - a)[0], + min = values.map(d => d.timestamp).sort((a, b) => a - b)[0]; let diagram = selection.select(`svg[data-d3-datum='${datum.fullName}']`); if (diagram.empty()) { diagram = selection .append("svg") - .classed({ timeline: true }) .attr("data-d3-datum", datum.fullName) - .attr("width", width) - .attr("height", height); - diagram - .append("line") - .attr("x1", chartPadding) - .attr("y1", height / 2) - .attr("x2", width - chartPadding) - .attr("y2", height / 2); - diagram + .classed({ timeline: true }); + } + diagram + .attr("width", chartSettings.width) + .attr("height", chartSettings.height); + + let lineAcross = diagram.select("line[data-d3-role='across']"); + if (lineAcross.empty()) { + lineAcross = diagram.append("line").attr("data-d3-role", "across"); + } + lineAcross + .attr("x1", chartSettings.chartPadding) + .attr("y1", chartSettings.height / 2) + .attr("x2", chartSettings.width - chartSettings.chartPadding) + .attr("y2", chartSettings.height / 2); + + let leftLineThrough = diagram.select("line[data-d3-role='left-through']"); + if (leftLineThrough.empty()) { + leftLineThrough = diagram .append("line") - .attr("x1", chartPadding + 10) - .attr("y1", height / 2 - 10) - .attr("x2", chartPadding + 10) - .attr("y2", height / 2 + 10); - diagram + .attr("data-d3-role", "left-through"); + } + leftLineThrough + .attr("x1", chartSettings.chartPadding + chartSettings.verticalLineXOffset) + .attr("y1", chartSettings.height / 2 - chartSettings.verticalLineYOffset) + .attr("x2", chartSettings.chartPadding + chartSettings.verticalLineXOffset) + .attr("y2", chartSettings.height / 2 + chartSettings.verticalLineYOffset); + + let rightLineThrough = diagram.select("line[data-d3-role='right-through']"); + if (rightLineThrough.empty()) { + rightLineThrough = diagram .append("line") - .attr("x1", width - chartPadding - 10) - .attr("y1", height / 2 - 10) - .attr("x2", width - chartPadding - 10) - .attr("y2", height / 2 + 10); + .attr("data-d3-role", "right-through"); } + rightLineThrough + .attr( + "x1", + chartSettings.width - + chartSettings.chartPadding - + chartSettings.verticalLineXOffset + ) + .attr("y1", chartSettings.height / 2 - chartSettings.verticalLineYOffset) + .attr( + "x2", + chartSettings.width - + chartSettings.chartPadding - + chartSettings.verticalLineXOffset + ) + .attr("y2", chartSettings.height / 2 + chartSettings.verticalLineYOffset); let code = selection.select("pre"); if (code.empty()) { @@ -516,7 +572,12 @@ function createOrUpdateEventChart(selection, datum) { const xFn = d3.scale .linear() .domain([min, max]) - .range([10 + chartPadding, width - chartPadding - 10]); + .range([ + chartSettings.verticalLineXOffset + chartSettings.chartPadding, + chartSettings.width - + chartSettings.chartPadding - + chartSettings.verticalLineXOffset, + ]); let eventsContainer = diagram.select("g.events"); if (eventsContainer.empty()) { @@ -533,10 +594,7 @@ function createOrUpdateEventChart(selection, datum) { .classed({ event: true }) .attr("tabindex", 0); - newEvents - .append("circle") - .attr("cy", height / 2) - .attr("r", circleRadius); + newEvents.append("circle"); function focusStart(_) { this.classList.add("hovered"); @@ -559,7 +617,7 @@ function createOrUpdateEventChart(selection, datum) { const text = this.appendChild( document.createElementNS("http://www.w3.org/2000/svg", "text") ); - text.setAttribute("y", height / 2 + 25); + text.setAttribute("y", chartSettings.height / 2 + 25); text.setAttribute( "x", xFn(dataPoint.timestamp) - `${dataPoint.timestamp}`.length * 4.5 @@ -569,7 +627,11 @@ function createOrUpdateEventChart(selection, datum) { events.attr("data-d3-datum", d => `${d.fullName}-${d.index}-${d.timestamp}`); - events.selectAll("circle").attr("cx", d => xFn(d.timestamp)); + events + .selectAll("circle") + .attr("cy", chartSettings.height / 2) + .attr("cx", d => xFn(d.timestamp)) + .attr("r", chartSettings.circleRadius); events .on("focusin", select) @@ -580,6 +642,152 @@ function createOrUpdateEventChart(selection, datum) { events.exit().remove(); } +const METRICS_TABLE_SETTINGS_KEY = "about-glean-metrics-table-settings"; +/** + * When adding new fields to the metrics table settings, + * a corresponding element matching the query selector + * `[data-form-control=<key>]` must be present in the DOM. + */ +const metricsTableSettings = { + hideEmptyValueRows: false, + histograms: { + boxPadding: 5, + chartMax: 150, + leftPadding: 20, + chartPadding: 50, + scaledMax: 110, + }, + timelines: { + height: 75, + width: 500, + chartPadding: 50, + circleRadius: 6, + verticalLineXOffset: 10, + verticalLineYOffset: 10, + }, +}; + +function initMetricsSettings() { + const handleSetting = (obj, key, parent) => { + let element = parent.querySelector(`[data-form-control='${key}']`); + let valueFn = e => e.target.value; + if (!element && typeof obj[key] !== "object") { + console.error( + new Error( + `Unable to find form control with key '${key}' in the parent element` + ), + parent + ); + return; + } + switch (typeof obj[key]) { + case "boolean": + valueFn = e => e.target.checked; + obj[key] = valueFn({ target: element }); + break; + case "object": + element = parent.querySelector(`[data-form-group='${key}']`); + if (!element) { + console.error( + new Error( + `Unable to find form control with key '${key}' in the parent element` + ), + parent + ); + return; + } + for (const subKey of Object.keys(obj[key])) { + handleSetting(obj[key], subKey, element); + } + break; + case "number": + valueFn = e => parseInt(e.target.value); + // eslint-disable-next-line no-fallthrough + default: + if (element.type !== typeof obj[key]) { + console.warn( + new Error( + `Form control input type does not match JavaScript value type ${typeof obj[key]}` + ) + ); + } + if (valueFn({ target: element })) { + obj[key] = valueFn({ target: element }); + } else { + element.value = obj[key]; + } + } + element.addEventListener("input", handleSettingChange(obj, valueFn)); + }; + + for (const key of Object.keys(metricsTableSettings)) { + handleSetting( + metricsTableSettings, + key, + document.getElementById("metrics-table-settings") + ); + } +} + +function handleSettingChange(obj, valueFn) { + return e => { + obj[e.target.getAttribute("data-form-control")] = valueFn(e); + settingsChanged(); + }; +} + +function settingsChanged() { + createOrUpdateHistogram( + d3.select("#metrics-table-settings-histogram-example"), + { + fullName: "histogram-example", + value: { + values: { + 0: 1, + 1: 5, + 2: 4, + 3: 0, + }, + }, + } + ); + + createOrUpdateEventChart( + d3.select("#metrics-table-settings-timeline-example"), + { + fullName: "timeline-example", + value: [ + { + timestamp: 0, + extra: { + value: 1, + }, + }, + { + timestamp: 1, + extra: { + value: 2, + }, + }, + { + timestamp: 4, + extra: { + value: 3, + }, + }, + { + timestamp: 8, + extra: { + value: 4, + }, + }, + ], + } + ); + + updateTable(); +} + function updateValueSelection(selection) { // Set the `data-l10n-id` attribute to the appropriate warning if the value is invalid, otherwise // unset it by returning `null`. @@ -686,7 +894,15 @@ function prettyPrint(jsonValue) { function updateTable() { LIMITED_METRIC_DATA = FILTERED_METRIC_DATA.toSorted((a, b) => d3.ascending(a.fullName, b.fullName) - ).filter((_, i) => i >= LIMIT_OFFSET && i < LIMIT_COUNT + LIMIT_OFFSET); + ) + // Filter out rows whose datum elements have either a) been loaded and have a value, or b) have not yet been loaded. + .filter(d => + metricsTableSettings.hideEmptyValueRows + ? (d.value !== undefined && d.value !== null) || !d.loaded + : true + ) + // Filter down to only the datum elements whose indexes fall in the limit offset+count. + .filter((_, i) => i >= LIMIT_OFFSET && i < LIMIT_COUNT + LIMIT_OFFSET); // Let's talk about d3.js // `d3.select` is a rough equivalent to `document.querySelector`, but the resulting object(s) are things d3 knows how to manipulate. @@ -813,9 +1029,9 @@ function updateFilteredMetricData(searchString) { }; FILTERED_METRIC_DATA = MAPPED_METRIC_DATA.filter( datum => - datum.category.includes(searchString) || - datum.name.includes(searchString) || - datum.type.includes(searchString) || + datum.category.toLowerCase().includes(searchString) || + datum.name.toLowerCase().includes(searchString) || + datum.type.toLowerCase().includes(searchString) || simpleTypeValueSearch(datum) ); } diff --git a/toolkit/content/tests/browser/browser_about_glean.js b/toolkit/content/tests/browser/browser_about_glean.js @@ -447,3 +447,91 @@ add_task(async function test_about_glean_ping_groups_and_none_label() { }); }); }); + +add_task(async function test_about_glean_metrics_table_settings() { + Services.fog.testResetFOG(); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("about.glean.redesign.enabled"); + }); + Services.prefs.setBoolPref("about.glean.redesign.enabled", true); + + await BrowserTestUtils.withNewTab("about:glean", async browser => { + await ContentTask.spawn(browser, null, async function () { + const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" + ); + + content.document.getElementById("category-metrics-table").click(); + + let tableBody; + const fetchTableBody = () => { + tableBody = content.document.getElementById("metrics-table-body"); + }; + fetchTableBody(); + let currentFirstChild = + tableBody.children[0].attributes["data-d3-row"].value; + + const tableFirstChildChanged = () => { + fetchTableBody(); + if ( + currentFirstChild != + tableBody.children[0].attributes["data-d3-row"].value + ) { + currentFirstChild = + tableBody.children[0].attributes["data-d3-row"].value; + return true; + } + return false; + }; + + const input = content.document.getElementById("filter-metrics"); + input.value = "aBool"; + input.dispatchEvent(new Event("input")); + + await TestUtils.waitForCondition( + tableFirstChildChanged, + "Wait for the table's first child to change", + 100, + 3 + ); + + content.document + .querySelector( + "[data-d3-row='testOnlyIpc.aBool'] button[data-l10n-id='about-glean-button-load-value']" + ) + .click(); + + content.document.getElementById("metrics-table-settings-button").click(); + + const checkbox = content.document.getElementById( + "settings-hide-empty-value-rows" + ); + checkbox.checked = true; + checkbox.dispatchEvent(new Event("input")); + + const tableNoChildren = () => { + fetchTableBody(); + if (tableBody.children.length === 0) { + return true; + } + return false; + }; + + await TestUtils.waitForCondition( + tableNoChildren, + "Wait for the table's children to be empty", + 100, + 3 + ); + + Assert.equal( + content.document.querySelector("[data-d3-row='testOnlyIpc.aBool']"), + null, + "Data row for `testOnlyIpc.aBool` should not exist" + ); + }); + }); +}); diff --git a/toolkit/locales/en-US/toolkit/about/aboutGlean.ftl b/toolkit/locales/en-US/toolkit/about/aboutGlean.ftl @@ -169,6 +169,36 @@ about-glean-metrics-table-header-type = Type about-glean-metrics-table-header-value = Value # This message refers to the UI action buttons for a given metric. about-glean-metrics-table-header-actions = Actions +about-glean-metrics-table-settings-button = Settings + +# Settings for the metrics table and its visualizations in about:glean +about-glean-metrics-table-settings-title = Metrics Table Settings +about-glean-metrics-table-settings-category-general = General +about-glean-metrics-table-settings-hide-empty-value-rows = Hide empty value rows + +about-glean-metrics-table-settings-category-visualizations = Visualizations +# This is a heading that is immediately followed by an example data visualization +about-glean-metrics-table-settings-visualization-example = Example + +about-glean-metrics-table-settings-category-visualizations-histogram = Histogram +about-glean-metrics-table-settings-histograms-chart-max = Chart maximum height +# The maximum height after to which the y-values on the chart will be scaled +about-glean-metrics-table-settings-histograms-scaled-max = Scaled maximum height +about-glean-metrics-table-settings-histograms-box-padding = Box padding +about-glean-metrics-table-settings-histograms-chart-padding = Chart padding +about-glean-metrics-table-settings-histograms-left-padding = Additional left padding + +about-glean-metrics-table-settings-category-visualizations-timeline = Timeline +about-glean-metrics-table-settings-timelines-height = Height +about-glean-metrics-table-settings-timelines-width = Width +about-glean-metrics-table-settings-timelines-chart-padding = Chart padding +# The radius of each circle denoting individual events recorded for an event metric +about-glean-metrics-table-settings-timelines-circle-radius = Circle radius +# The offset on the x-axis from the end of the horizontal line for the y-axis line +about-glean-metrics-table-settings-timelines-vertical-line-x-offset = Y-axis X offset +# The offset on the y-axis from the x-axis for the y-axis line +about-glean-metrics-table-settings-timelines-vertical-line-y-offset = Y-axis Y offset + # Label displayed near an input field that can be used to filter metrics about-glean-label-for-filter-metrics = Filter