@neo4j-ndl/react-charts
Version:
React implementation of charts from Neo4j Design System
231 lines • 9.14 kB
JavaScript
;
/**
*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateThresholdAriaDescription = generateThresholdAriaDescription;
const defaults_1 = require("./defaults");
const threshold_1 = require("./threshold");
/**
* Extract numeric y-values from inline series data.
* Handles formats: [x, y], [x, y, ...], or single number.
*/
function extractInlineValues(data, encode) {
if (!Array.isArray(data)) {
return [];
}
const yIndex = (encode === null || encode === void 0 ? void 0 : encode.y) !== undefined && typeof encode.y === 'number' ? encode.y : 1;
const values = [];
for (const point of data) {
if (point === null || point === undefined) {
continue;
}
if (Array.isArray(point)) {
const val = point[yIndex];
if (typeof val === 'number' && !isNaN(val)) {
values.push(val);
}
}
else if (typeof point === 'number' && !isNaN(point)) {
values.push(point);
}
else if (typeof point === 'object' && 'value' in point) {
const val = point.value;
if (typeof val === 'number' && !isNaN(val)) {
values.push(val);
}
else if (Array.isArray(val)) {
const v = val[yIndex];
if (typeof v === 'number' && !isNaN(v)) {
values.push(v);
}
}
}
}
return values;
}
/**
* Extract numeric y-values from a dataset source for a given series.
*/
function extractDatasetValues(dataset, series) {
if (!dataset) {
return [];
}
const datasets = Array.isArray(dataset) ? dataset : [dataset];
const datasetIndex = 'datasetIndex' in series && typeof series.datasetIndex === 'number'
? series.datasetIndex
: 0;
const currentDataset = datasets[datasetIndex];
if (!currentDataset || !Array.isArray(currentDataset.source)) {
return [];
}
const source = currentDataset.source;
if (source.length < 1) {
return [];
}
// Determine column names and data start index.
// When `dimensions` is provided, column names come from there and all
// rows in `source` are data. Otherwise the first row is a header.
const dimensions = currentDataset.dimensions;
let columnNames;
let dataStartIndex;
if (Array.isArray(dimensions) && dimensions.length > 0) {
columnNames = dimensions.map((d) => (typeof d === 'string' ? d : d.name));
dataStartIndex = 0;
}
else {
const headerRow = source[0];
if (!Array.isArray(headerRow) || source.length < 2) {
return [];
}
columnNames = headerRow;
dataStartIndex = 1;
}
// Determine which column contains the y-values
const encode = 'encode' in series ? series.encode : undefined;
let yColumnIndex = -1;
if ((encode === null || encode === void 0 ? void 0 : encode.y) !== undefined) {
if (typeof encode.y === 'number') {
yColumnIndex = encode.y;
}
else if (typeof encode.y === 'string') {
yColumnIndex = columnNames.indexOf(encode.y);
}
}
if (yColumnIndex === -1) {
// Avoid guessing a data column for aria descriptions.
// If we cannot resolve a value column explicitly, return no values.
return [];
}
const values = [];
for (let i = dataStartIndex; i < source.length; i++) {
const row = source[i];
if (!Array.isArray(row)) {
continue;
}
const val = row[yColumnIndex];
if (typeof val === 'number' && !isNaN(val)) {
values.push(val);
}
}
return values;
}
function pluralize(count, singular, plural) {
return count === 1 ? singular : (plural !== null && plural !== void 0 ? plural : `${singular}s`);
}
/**
* Resolves y-values for a series from either inline data or a dataset.
*/
function resolveSeriesValues(series, dataset) {
if ('data' in series && series.data) {
const encode = 'encode' in series
? series.encode
: undefined;
return extractInlineValues(series.data, encode);
}
return extractDatasetValues(dataset, series);
}
/**
* Generates a human-readable aria-label description for the chart
* that includes meaningful threshold context and data relationships.
*/
function generateThresholdAriaDescription(propsSeries, dataset) {
var _a, _b, _c;
const seriesArray = Array.isArray(propsSeries) ? propsSeries : [propsSeries];
// Separate threshold lines from data series
const thresholds = [];
const dataSeries = [];
for (const series of seriesArray) {
if (series.type === 'thresholdLine') {
const threshold = series;
thresholds.push({
condition: (_a = threshold.condition) !== null && _a !== void 0 ? _a : defaults_1.defaultThresholdLineSeriesOption.condition,
customConditionText: threshold.customConditionText,
hasCustomCondition: typeof threshold.customCondition === 'function',
notificationType: threshold.notificationType,
yAxisValue: threshold.yAxis,
});
}
else {
const echartsSeriesOption = series;
const name = typeof echartsSeriesOption.name === 'string'
? echartsSeriesOption.name
: `Series ${dataSeries.length + 1}`;
const values = resolveSeriesValues(echartsSeriesOption, dataset);
dataSeries.push({
name,
type: (_b = echartsSeriesOption.type) !== null && _b !== void 0 ? _b : 'line',
values,
});
}
}
if (thresholds.length === 0) {
return undefined;
}
const parts = [];
// 1. Overview
const dataSeriesCountText = dataSeries.length === 1
? '1 data series'
: `${dataSeries.length} data series`;
const thresholdCountText = thresholds.length === 1
? '1 threshold line'
: `${thresholds.length} threshold lines`;
parts.push(`This chart has ${dataSeriesCountText} and ${thresholdCountText}.`);
// 2. Describe each data series
for (const series of dataSeries) {
const { name, type, values } = series;
if (values.length > 0) {
const min = Math.min(...values);
const max = Math.max(...values);
parts.push(`${name} is a ${type} chart with ${values.length} ${pluralize(values.length, 'data point')} ranging from ${min} to ${max}.`);
}
else {
parts.push(`${name} is a ${type} chart with no resolved data.`);
}
}
// 3. Describe threshold lines
for (const threshold of thresholds) {
const conditionText = threshold.hasCustomCondition
? ((_c = threshold.customConditionText) !== null && _c !== void 0 ? _c : 'custom condition')
: (0, threshold_1.thresholdConditionToText)(threshold.condition);
parts.push(`${threshold.notificationType === 'warning' ? 'Warning' : 'Danger'} threshold at ${threshold.yAxisValue}: triggers when values are ${conditionText} ${threshold.yAxisValue}.`);
}
// 4. Report threshold violations per data series
for (const series of dataSeries) {
const { name, values } = series;
if (values.length === 0) {
continue;
}
for (const threshold of thresholds) {
// Custom conditions use runtime callbacks that we cannot evaluate
// at description-generation time, so we skip violation reporting.
if (threshold.hasCustomCondition) {
continue;
}
const violatingValues = values.filter((v) => (0, threshold_1.evaluateThresholdCondition)(v, threshold.condition, threshold.yAxisValue).isConditionMet);
if (violatingValues.length > 0) {
const count = violatingValues.length;
parts.push(`${name} has ${count} ${pluralize(count, 'value')} ${(0, threshold_1.thresholdConditionToText)(threshold.condition)} the ${threshold.notificationType} threshold of ${threshold.yAxisValue}.`);
}
}
}
return parts.join(' ');
}
//# sourceMappingURL=aria-description.js.map