UNPKG

@czi-sds/data-viz

Version:
471 lines (454 loc) 19.3 kB
"use client"; 'use strict'; 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;