@czi-sds/data-viz
Version:
2023 Science Initiative Data Visualization Component Library
471 lines (454 loc) • 19.3 kB
JavaScript
"use client";
;
var jsxRuntime = require('react/jsx-runtime');
var echarts = require('echarts');
var react = require('react');
var lodash = require('lodash');
var styled = require('@emotion/styled');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var styled__default = /*#__PURE__*/_interopDefault(styled);
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __rest(s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
}
function __makeTemplateObject(cooked, raw) {
if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
return cooked;
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var EMPTY_OBJECT = {};
var DEFAULT_ITEM_STYLE = {
color: function () {
return "rgb(0, 0, 0)";
},
};
function createChartOptions(props) {
var axisPointer = props.axisPointer, camera = props.camera, data = props.data, dataZoom = props.dataZoom, emphasis = props.emphasis, encode = props.encode, gridProp = props.grid, _a = props.itemStyle, itemStyle = _a === void 0 ? DEFAULT_ITEM_STYLE : _a, options = props.options, symbolSize = props.symbolSize, _b = props.symbol, symbol = _b === void 0 ? "rect" : _b;
var _c = generateDefaultValues(props), defaultAxisPointer = _c.defaultAxisPointer, defaultDataZoom = _c.defaultDataZoom, defaultEmphasis = _c.defaultEmphasis, defaultGrid = _c.defaultGrid, defaultXAxis = _c.defaultXAxis, defaultYAxis = _c.defaultYAxis;
var customGrid = typeof gridProp === "function" ? gridProp(defaultGrid) : gridProp;
var _d = options || {}, optionsAxisPointer = _d.axisPointer, optionsDataZoom = _d.dataZoom, optionsSeries = _d.series, optionsXAxis = _d.xAxis, optionsYAxis = _d.yAxis, optionsRest = __rest(_d, ["axisPointer", "dataZoom", "series", "xAxis", "yAxis"]);
return __assign({ animation: false, axisPointer: mergeAxisPointer(defaultAxisPointer, axisPointer, optionsAxisPointer), dataZoom: (camera === null || camera === void 0 ? void 0 : camera.active)
? mergeDataZoom(defaultDataZoom, dataZoom, optionsDataZoom)
: [], dataset: {
source: data,
}, grid: customGrid || defaultGrid, series: [
Object.assign({
emphasis: Object.assign(defaultEmphasis, emphasis),
encode: encode,
itemStyle: itemStyle,
legendHoverLink: false,
symbol: symbol,
symbolSize: symbolSize,
}, optionsSeries
? Array.isArray(optionsSeries)
? optionsSeries[0]
: optionsSeries
: [], { symbol: symbol, type: "scatter" }),
], xAxis: [
Object.assign(defaultXAxis, optionsXAxis
? Array.isArray(optionsXAxis)
? optionsXAxis[0]
: optionsXAxis
: {}),
], yAxis: [
Object.assign(defaultYAxis, optionsYAxis
? Array.isArray(optionsYAxis)
? optionsYAxis[0]
: optionsYAxis
: {}),
] }, optionsRest);
}
function mergeAxisPointer(defaultAxisPointer, axisPointer, optionsAxisPointer) {
var finalAxisPointer = Array.isArray(axisPointer)
? axisPointer // if it's an array, assume that the user has [{...propsToChangeOnX}] OR [ , {...propsToChangeOnY}] OR [{...propsToChangeOnX}, {...propsToChangeOnY}]
: [axisPointer, axisPointer]; // else if the user only supplies one AxisPointer object, we assume they want the props to apply to both x and y axisPointer objects
var finalOptionsAxisPointer = Array.isArray(optionsAxisPointer)
? optionsAxisPointer
: [optionsAxisPointer, optionsAxisPointer];
// merge x axisPointer options
var x = __assign(__assign(__assign({}, defaultAxisPointer), finalAxisPointer[0]), finalOptionsAxisPointer === null || finalOptionsAxisPointer === void 0 ? void 0 : finalOptionsAxisPointer[0]);
// merge y axisPointer options
var y = __assign(__assign(__assign({}, defaultAxisPointer), finalAxisPointer[1]), finalOptionsAxisPointer[1]);
return [x, y];
}
function mergeDataZoom(defaultDataZoom, dataZoom, optionsDataZoom) {
var finalDataZoom = Array.isArray(dataZoom)
? dataZoom // if it's an array, assume that the user has [{...propsToChangeOnX}] OR [ , {...propsToChangeOnY}] OR [{...propsToChangeOnX}, {...propsToChangeOnY}]
: [dataZoom, dataZoom]; // else if the user only supplies one dataZoom object, we assume they want the props to apply to both x and y zoom objects
var finalOptionsDataZoom = Array.isArray(optionsDataZoom)
? optionsDataZoom
: [optionsDataZoom, optionsDataZoom];
// merge x dataZoom options
var x = __assign(__assign(__assign({}, defaultDataZoom[0]), finalDataZoom[0]), finalOptionsDataZoom === null || finalOptionsDataZoom === void 0 ? void 0 : finalOptionsDataZoom[0]);
// merge y dataZoom options
var y = __assign(__assign(__assign({}, defaultDataZoom[1]), finalDataZoom[1]), finalOptionsDataZoom[1]);
return [x, y];
}
function generateDefaultValues(props) {
var camera = props.camera, height = props.height, symbol = props.symbol, width = props.width, xAxisData = props.xAxisData, yAxisData = props.yAxisData;
var defaultGrid = {
height: "".concat(height, "px"),
left: 0,
top: 0,
// (atarashansky): this is the key change to align x and y axis
// labels to fixed spaces
width: "".concat(width, "px"),
};
var defaultAxisPointer = {
label: { show: false },
show: false,
triggerOn: "mousemove",
};
var defaultXAxis = {
axisLabel: { fontSize: 0, rotate: 90 },
axisLine: {
show: false,
},
axisTick: {
show: false,
},
boundaryGap: true,
data: xAxisData,
splitLine: {
show: false,
},
type: "category",
};
var defaultYAxis = {
axisLabel: { fontSize: 0 },
axisLine: {
show: false,
},
axisTick: {
show: false,
},
boundaryGap: true,
data: yAxisData,
splitLine: {
show: false,
},
};
var defaultEmphasis = {
itemStyle: {
borderColor: symbol === "circle" ? "black" : "white",
borderType: "solid",
borderWidth: symbol === "circle" ? 2 : 4,
opacity: 1,
},
scale: false,
};
var defaultCamera = {
height: camera && camera.height ? camera.height : 20,
width: camera && camera.width ? camera.width : 40,
};
var defaultDataZoom = [
{
// end index of the x axis window
endValue: defaultCamera.width - 1,
filterMode: "filter",
moveOnMouseMove: true,
// There's a PR to allow touchpad panning
// https://github.com/apache/echarts/pull/19352
moveOnMouseWheel: false,
orient: "horizontal",
preventDefaultMouseMove: true,
// start index of the x axis window
startValue: 0,
throttle: 0,
type: "inside",
xAxisIndex: 0,
zoomOnMouseWheel: false,
},
{
// end index of the y axis window
endValue: defaultCamera.height - 1,
filterMode: "filter",
moveOnMouseMove: true,
moveOnMouseWheel: true,
orient: "vertical",
preventDefaultMouseMove: true,
// start index of the y axis window
startValue: 0,
throttle: 0,
type: "inside",
yAxisIndex: 0,
zoomOnMouseWheel: false,
},
];
return {
defaultAxisPointer: defaultAxisPointer,
defaultDataZoom: defaultDataZoom,
defaultEmphasis: defaultEmphasis,
defaultGrid: defaultGrid,
defaultXAxis: defaultXAxis,
defaultYAxis: defaultYAxis,
};
}
var UPDATE_THROTTLE_MS = 1 * 100;
function useUpdateChart(_a) {
var axisPointer = _a.axisPointer, camera = _a.camera, chart = _a.chart, data = _a.data, emphasis = _a.emphasis, xAxisData = _a.xAxisData, yAxisData = _a.yAxisData, width = _a.width, height = _a.height, encode = _a.encode, itemStyle = _a.itemStyle, symbol = _a.symbol, symbolSize = _a.symbolSize, grid = _a.grid, options = _a.options, onEvents = _a.onEvents;
var throttledUpdateChart = react.useMemo(function () {
return lodash.throttle(function () {
if (!chart || !data || !xAxisData || !yAxisData) {
return;
}
// (thuang): resize() needs to be called before setOption() to prevent
// TypeError: Cannot read properties of undefined (reading 'shouldBePainted')
chart.resize();
var chartOptions = createChartOptions({
axisPointer: axisPointer,
camera: camera,
data: data,
emphasis: emphasis,
encode: encode,
grid: grid,
height: height,
itemStyle: itemStyle,
options: options,
symbol: symbol,
symbolSize: symbolSize,
width: width,
xAxisData: xAxisData,
yAxisData: yAxisData,
});
chart.setOption(chartOptions, {
replaceMerge: ["dataZoom", "tooltip"],
});
/**
* We need to remove old event listeners and bind new ones to
* make sure that the event listeners are updated when the props change.
*/
if (onEvents) {
var _loop_1 = function (eventName) {
if (Object.prototype.hasOwnProperty.call(onEvents, eventName) &&
typeof eventName === "string" &&
typeof onEvents[eventName] === "function") {
// Remove old event listener
chart.off(eventName);
// Add new event listener
chart.on(eventName, function (event) {
onEvents[eventName](event, chart);
});
}
};
for (var eventName in onEvents) {
_loop_1(eventName);
}
}
}, UPDATE_THROTTLE_MS,
// (thuang): Trailing guarantees that the last call to the function will
// be executed
{ trailing: true });
}, [
axisPointer,
camera,
chart,
data,
emphasis,
xAxisData,
yAxisData,
width,
height,
encode,
itemStyle,
symbol,
symbolSize,
grid,
options,
onEvents,
]);
react.useEffect(function () {
return function () { return throttledUpdateChart.cancel(); };
}, [throttledUpdateChart]);
// Update the charts
react.useEffect(function () {
throttledUpdateChart();
}, [
axisPointer,
camera,
chart,
data,
emphasis,
xAxisData,
yAxisData,
throttledUpdateChart,
width,
height,
encode,
itemStyle,
symbol,
symbolSize,
grid,
options,
onEvents,
]);
}
var ChartContainer = styled__default.default("div")(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n ", "\n"], ["\n ", "\n"])), getWidthAndHeight);
function getWidthAndHeight(_a) {
var width = _a.width, height = _a.height;
return "\n width: ".concat(width, "px;\n height: ").concat(height, "px;\n ");
}
var templateObject_1;
var HeatmapChart = react.forwardRef(function (props, ref
// eslint-disable-next-line sonarjs/cognitive-complexity
) {
var axisPointer = props.axisPointer, width = props.width, height = props.height, _a = props.echartsRendererMode, echartsRendererMode = _a === void 0 ? "svg" : _a, camera = props.camera, onEvents = props.onEvents, xAxisData = props.xAxisData, yAxisData = props.yAxisData, data = props.data, encode = props.encode, emphasis = props.emphasis, itemStyle = props.itemStyle, symbol = props.symbol, _b = props.symbolSize, symbolSize = _b === void 0 ? 5 : _b, grid = props.grid, options = props.options, rest = __rest(props, ["axisPointer", "width", "height", "echartsRendererMode", "camera", "onEvents", "xAxisData", "yAxisData", "data", "encode", "emphasis", "itemStyle", "symbol", "symbolSize", "grid", "options"]);
// Validate width and height
if (!width || !height) {
throw Error("Heatmap must have width and height bigger than Zero!");
}
// Ref for the chart container
var innerRef = react.useRef(null);
/**
* (thuang): We need both a state and a ref to store the chart instance, so
* some hooks can opt out of re-rendering when the chart instance changes.
*/
var _c = react.useState(null), chart = _c[0], setChart = _c[1];
var chartRef = react.useRef(chart);
/**
* (thuang): Use this ref to store the onEvents prop to prevent
* unnecessary re-renders when the onEvents prop changes.
* NOTE: This implies that `onEvents` prop changes alone from the parent
* won't re-render the chart
*/
var onEventsRef = react.useRef(onEvents);
/**
* (thuang): Use this function to dispose the chart instance for both
* the state and the ref. This is to prevent memory leaks.
*/
var disposeChart = react.useCallback(function () {
var _a;
(_a = chartRef.current) === null || _a === void 0 ? void 0 : _a.dispose();
chartRef.current = null;
setChart(null);
}, []);
// Function to initialize the chart
var initChart = react.useCallback(function () {
var onEventsCurrent = onEventsRef.current;
var current = innerRef.current;
if (!current ||
chartRef.current ||
// (thuang): echart's `init()` will throw error if the container has 0 width or height
(current === null || current === void 0 ? void 0 : current.getAttribute("height")) === "0" ||
(current === null || current === void 0 ? void 0 : current.getAttribute("width")) === "0") {
return;
}
// Initialize ECharts instance
var rawChart = echarts.init(current, EMPTY_OBJECT, {
renderer: echartsRendererMode,
useDirtyRect: true,
});
// Bind events if provided
if (onEventsCurrent) {
bindEvents(rawChart, onEventsCurrent);
}
setChart(rawChart);
chartRef.current = rawChart;
// Cleanup function
return function () {
disposeChart();
// Unbind events if provided
if (onEventsCurrent) {
for (var eventName in onEventsCurrent) {
if (Object.prototype.hasOwnProperty.call(onEventsCurrent, eventName) &&
typeof eventName === "string" &&
typeof onEventsCurrent[eventName] === "function") {
rawChart.off(eventName, onEventsCurrent[eventName]);
}
}
}
};
}, [echartsRendererMode, disposeChart]);
// Initialize charts on component mount
react.useEffect(function () {
disposeChart();
initChart();
}, [initChart, disposeChart]);
// Hook to update chart data and options
useUpdateChart({
axisPointer: axisPointer,
camera: camera,
chart: chart,
data: data,
emphasis: emphasis,
encode: encode,
grid: grid,
height: height,
itemStyle: itemStyle,
onEvents: onEvents,
options: options,
symbol: symbol,
symbolSize: symbolSize,
width: width,
xAxisData: xAxisData,
yAxisData: yAxisData,
});
// Render the chart container
return (jsxRuntime.jsx(ChartContainer, __assign({ height: height, width: width, ref: handleRef }, rest)));
// Function to bind events to the ECharts instance
function bindEvents(instance, events) {
function innerBindEvent(eventName, func) {
// Ignore invalid event configurations
if (typeof eventName === "string" && typeof func === "function") {
// Bind event
instance.on(eventName, function (param) {
func(param, instance);
});
}
}
// Loop through events and bind them
for (var eventName in events) {
if (Object.prototype.hasOwnProperty.call(events, eventName)) {
innerBindEvent(eventName, events[eventName]);
}
}
}
// Function to handle the ref of the chart container
function handleRef(element) {
innerRef.current = element;
if (!ref)
return;
// (thuang): `ref` from `forwardRef` can be a function or a ref object
if (typeof ref === "function") {
ref(element);
}
else {
ref.current = element;
}
}
});
var index = react.memo(HeatmapChart);
exports.HeatmapChart = index;