victory-native
Version:
A charting library for React Native with a focus on performance and customization.
276 lines (275 loc) • 12.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformInputData = void 0;
const getOffsetFromAngle_1 = require("../../utils/getOffsetFromAngle");
const tickHelpers_1 = require("../../utils/tickHelpers");
const getAxisLabelLayout_1 = require("./getAxisLabelLayout");
const getAxisTitleLayout_1 = require("./getAxisTitleLayout");
const getXScaleInputBounds_1 = require("./getXScaleInputBounds");
const getXAxisTicks_1 = require("./getXAxisTicks");
const getYScaleInputBounds_1 = require("./getYScaleInputBounds");
const getYScaleDomain_1 = require("./getYScaleDomain");
const makeScale_1 = require("./makeScale");
const getYOutputValue = (value, yScale, yAxisScale) => {
if (typeof value !== "number")
return value;
if (yAxisScale === "log" && value <= 0)
return null;
const output = yScale(value);
return Number.isFinite(output) ? output : null;
};
/**
* This is a fatty. Takes raw user input data, and transforms it into a format
* that's easier for us to consume. End result looks something like:
* {
* ix: [1, 2, 3], // input x values
* ox: [10, 20, 30], // canvas x values
* y: {
* high: { i: [3, 4, 5], o: [30, 40, 50] },
* low: { ... }
* }
* }
* This form allows us to easily e.g. do a binary search to find closest output x index
* and then map that into each of the other value lists.
*/
const transformInputData = ({ data: _data, xKey, yKeys, outputWindow, domain, domainPadding, xAxis, yAxes, viewport, labelRotate, axisScales, }) => {
var _a;
const data = [..._data];
const { xAxisScale = "linear", yAxisScale = "linear" } = axisScales || {};
// Determine if xKey data is numerical
const isNumericalData = data.every((datum) => typeof datum[xKey] === "number");
// and sort if it is
if (isNumericalData) {
data.sort((a, b) => +a[xKey] - +b[xKey]);
}
// // Set up our y-output data structure
const y = yKeys.reduce((acc, k) => {
acc[k] = { i: [], o: [] };
return acc;
}, {});
const rawChartWidth = outputWindow.xMax - outputWindow.xMin;
const xTickValues = xAxis === null || xAxis === void 0 ? void 0 : xAxis.tickValues;
const xTicks = xAxis === null || xAxis === void 0 ? void 0 : xAxis.tickCount;
const tickDomainsX = (0, tickHelpers_1.getDomainFromTicks)(xTickValues);
const ix = data.map((datum) => datum[xKey]);
const ixNum = ix.map((val, i) => (isNumericalData ? val : i));
const xInputBounds = (0, getXScaleInputBounds_1.getXScaleInputBounds)({
isNumericalData,
ixNum,
domain: domain === null || domain === void 0 ? void 0 : domain.x,
tickDomain: tickDomainsX,
});
const xTempScale = (0, makeScale_1.makeScale)({
inputBounds: xInputBounds,
outputBounds: [0, rawChartWidth],
axisScale: xAxisScale,
});
const xTicksForLabelLayout = (0, getXAxisTicks_1.getXAxisTicks)({
isNumericalData,
ix,
tickCount: xTicks,
tickValues: xTickValues,
xScale: xTempScale,
});
const xAxisLabelLayouts = xTicksForLabelLayout.map((xTick, index) => {
const labelInput = (isNumericalData ? xTick : ix[xTick]);
const labelValue = xAxis.formatXLabel
? xAxis.formatXLabel(labelInput)
: String(labelInput !== null && labelInput !== void 0 ? labelInput : xTick);
return (0, getAxisLabelLayout_1.getAxisLabelLayout)({
axis: "x",
orientation: "vertical",
value: labelInput,
text: String(labelValue),
index,
font: xAxis.font,
labelRenderer: xAxis.labelRenderer,
});
});
const maxXLabelLayout = (0, getAxisLabelLayout_1.getMaxAxisLabelLayout)(xAxisLabelLayouts);
const xAxisTitleLayout = (0, getAxisTitleLayout_1.getAxisTitleLayout)({
title: xAxis.title,
font: xAxis.font,
});
const xAxisTitleOutset = xAxisTitleLayout.hasContent
? xAxisTitleLayout.height + xAxisTitleLayout.offset
: 0;
// workt with adjustedoutputwindow isntead of directly
// working with outpuwidnow
const adjustedOutputWindow = Object.assign({}, outputWindow);
if (labelRotate && xAxis.labelPosition === "outset") {
const rotateOffset = Math.abs(maxXLabelLayout.width * (0, getOffsetFromAngle_1.getOffsetFromAngle)(labelRotate));
if (xAxis.axisSide === "bottom") {
adjustedOutputWindow.yMax -= rotateOffset;
}
else if (xAxis.axisSide === "top") {
adjustedOutputWindow.yMin += rotateOffset;
}
}
// 1. Set up our y axes first...
// Transform data for each y-axis configuration
const yAxesTransformed = (_a = (yAxes !== null && yAxes !== void 0 ? yAxes : [{}])) === null || _a === void 0 ? void 0 : _a.map((yAxis) => {
var _a;
const yTickValues = yAxis.tickValues;
const yTicks = yAxis.tickCount;
const tickDomainsY = yAxis.domain
? yAxis.domain
: (0, tickHelpers_1.getDomainFromTicks)(yAxis.tickValues);
const yKeysForAxis = (_a = yAxis.yKeys) !== null && _a !== void 0 ? _a : yKeys;
const { yMin, yMax } = (0, getYScaleInputBounds_1.getYScaleInputBounds)({
data,
yKeys: yKeysForAxis,
domain: domain === null || domain === void 0 ? void 0 : domain.y,
tickDomain: tickDomainsY,
});
const yScaleDomain = (0, getYScaleDomain_1.getYScaleDomain)({ yMin, yMax, yAxisScale });
const yScaleRange = (() => {
var _a, _b;
const xTickCount = (_a = xAxis === null || xAxis === void 0 ? void 0 : xAxis.tickCount) !== null && _a !== void 0 ? _a : 0;
const xLabelOffset = (_b = xAxis === null || xAxis === void 0 ? void 0 : xAxis.labelOffset) !== null && _b !== void 0 ? _b : 0;
const xAxisSide = xAxis === null || xAxis === void 0 ? void 0 : xAxis.axisSide;
const xLabelPosition = xAxis === null || xAxis === void 0 ? void 0 : xAxis.labelPosition;
const xLabelOutset = xTickCount > 0 && maxXLabelLayout.width > 0
? maxXLabelLayout.height + xLabelOffset * 2
: 0;
const xAxisOutset = xAxisTitleOutset + (xLabelPosition === "outset" ? xLabelOutset : 0);
if (xAxisSide === "bottom") {
return [
adjustedOutputWindow.yMin,
adjustedOutputWindow.yMax - xAxisOutset,
];
}
if (xAxisSide === "top") {
return [
adjustedOutputWindow.yMin + xAxisOutset,
adjustedOutputWindow.yMax,
];
}
return [adjustedOutputWindow.yMin, adjustedOutputWindow.yMax];
})();
const yScale = (0, makeScale_1.makeScale)({
inputBounds: yScaleDomain,
outputBounds: yScaleRange,
// Reverse viewport y values since canvas coordinates increase downward
viewport: (viewport === null || viewport === void 0 ? void 0 : viewport.y) ? [viewport.y[1], viewport.y[0]] : undefined,
isNice: true,
padEnd: typeof domainPadding === "number"
? domainPadding
: domainPadding === null || domainPadding === void 0 ? void 0 : domainPadding.bottom,
padStart: typeof domainPadding === "number" ? domainPadding : domainPadding === null || domainPadding === void 0 ? void 0 : domainPadding.top,
axisScale: yAxisScale,
});
const yData = yKeysForAxis.reduce((acc, key) => {
acc[key] = {
i: data.map((datum) => datum[key]),
o: data.map((datum) => getYOutputValue(datum[key], yScale, yAxisScale)),
};
return acc;
}, {});
const yTicksNormalized = yTickValues
? (0, tickHelpers_1.downsampleTicks)(yTickValues, yTicks)
: yScale.ticks(yTicks);
yKeys.forEach((yKey) => {
if (yKeysForAxis.includes(yKey)) {
y[yKey].i = data.map((datum) => datum[yKey]);
y[yKey].o = data.map((datum) => getYOutputValue(datum[yKey], yScale, yAxisScale));
}
});
const yAxisLabelLayouts = yTicksNormalized.map((yTick, index) => {
var _a, _b;
const labelInput = yTick;
const label = (_b = (_a = yAxis === null || yAxis === void 0 ? void 0 : yAxis.formatYLabel) === null || _a === void 0 ? void 0 : _a.call(yAxis, labelInput)) !== null && _b !== void 0 ? _b : String(yTick);
return (0, getAxisLabelLayout_1.getAxisLabelLayout)({
axis: "y",
orientation: "vertical",
value: labelInput,
text: String(label),
index,
font: yAxis.font,
labelRenderer: yAxis.labelRenderer,
});
});
const maxYLabel = (0, getAxisLabelLayout_1.getMaxAxisLabelLayout)(yAxisLabelLayouts).width;
const yAxisTitleLayout = (0, getAxisTitleLayout_1.getAxisTitleLayout)({
title: yAxis.title,
font: yAxis.font,
});
const yAxisTitleOutset = yAxisTitleLayout.hasContent
? yAxisTitleLayout.height + yAxisTitleLayout.offset
: 0;
return {
yScale,
yTicksNormalized,
yData,
maxYLabel,
yAxisTitleOutset,
};
});
// 2. Then set up our x axis...
// Determine the x-output range based on yAxes/label options
const oRange = (() => {
let xMinAdjustment = 0;
let xMaxAdjustment = 0;
yAxes === null || yAxes === void 0 ? void 0 : yAxes.forEach((axis, index) => {
var _a, _b, _c, _d;
const yTickCount = axis.tickCount;
const yLabelPosition = axis.labelPosition;
const yAxisSide = axis.axisSide;
const yLabelOffset = axis.labelOffset;
// Calculate label width for this axis
const labelWidth = (_b = (_a = yAxesTransformed[index]) === null || _a === void 0 ? void 0 : _a.maxYLabel) !== null && _b !== void 0 ? _b : 0;
const yAxisTitleOutset = (_d = (_c = yAxesTransformed[index]) === null || _c === void 0 ? void 0 : _c.yAxisTitleOutset) !== null && _d !== void 0 ? _d : 0;
// Adjust xMin or xMax based on the axis side and label position
// make ajdustments for label rotation here
if (yAxisSide === "left") {
xMinAdjustment += yAxisTitleOutset;
if (yLabelPosition === "outset") {
xMinAdjustment +=
yTickCount > 0 && labelWidth > 0 ? labelWidth + yLabelOffset : 0;
}
}
else if (yAxisSide === "right") {
xMaxAdjustment -= yAxisTitleOutset;
if (yLabelPosition === "outset") {
xMaxAdjustment +=
yTickCount > 0 && labelWidth > 0 ? -labelWidth - yLabelOffset : 0;
}
}
});
// Return the adjusted output range
return [
adjustedOutputWindow.xMin + xMinAdjustment,
adjustedOutputWindow.xMax + xMaxAdjustment,
];
})();
const xScale = (0, makeScale_1.makeScale)({
// if single data point, manually add upper & lower bounds so chart renders properly
inputBounds: xInputBounds,
outputBounds: oRange,
viewport: viewport === null || viewport === void 0 ? void 0 : viewport.x,
padStart: typeof domainPadding === "number" ? domainPadding : domainPadding === null || domainPadding === void 0 ? void 0 : domainPadding.left,
padEnd: typeof domainPadding === "number" ? domainPadding : domainPadding === null || domainPadding === void 0 ? void 0 : domainPadding.right,
axisScale: xAxisScale,
});
// Normalize xTicks values either via the d3 scaleLinear ticks() function or our custom downSample function
// For consistency we do it here, so we have both y and x ticks to pass to the axis generator
const finalXTicksNormalized = (0, getXAxisTicks_1.getXAxisTicks)({
isNumericalData,
ix,
tickCount: xTicks,
tickValues: xTickValues,
xScale,
});
const ox = ixNum.map((x) => xScale(x));
return {
ix,
y,
isNumericalData,
ox,
xScale,
xTicksNormalized: finalXTicksNormalized,
// conform to type NonEmptyArray<T>
yAxes: [yAxesTransformed[0], ...yAxesTransformed.slice(1)],
};
};
exports.transformInputData = transformInputData;