diff --git a/CHANGELOG.md b/CHANGELOG.md
index b96416aa..0a71318a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
+### [13.0.4](https://github.com/eea/volto-plotlycharts/compare/13.0.3...13.0.4) - 20 August 2025
+
+#### :house: Internal changes
+
+- style: Automated code fix [eea-jenkins - [`b589483`](https://github.com/eea/volto-plotlycharts/commit/b589483620c63d6db22d3220d6ee47c2769a4c8e)]
+- style: Automated code fix [eea-jenkins - [`ebced8c`](https://github.com/eea/volto-plotlycharts/commit/ebced8c76b0b5ff98a94c8acfb4dec3034281b99)]
+- style: Automated code fix [eea-jenkins - [`bdcc74a`](https://github.com/eea/volto-plotlycharts/commit/bdcc74a77e5099bc24a4698e48433233b7ce8a9a)]
+- style: Automated code fix [eea-jenkins - [`7f88384`](https://github.com/eea/volto-plotlycharts/commit/7f88384ddd9a4eab862298e9ce6d199d11e68c6d)]
+- style: Automated code fix [eea-jenkins - [`55c075c`](https://github.com/eea/volto-plotlycharts/commit/55c075cad10cf2fe65421af49a2644aad62d1f6e)]
+
+#### :hammer_and_wrench: Others
+
+- hide embed data table [Miu Razvan - [`49ff16b`](https://github.com/eea/volto-plotlycharts/commit/49ff16b375923b94b3cb1f9b59e520f552607301)]
+- fix eslint [Miu Razvan - [`dc67756`](https://github.com/eea/volto-plotlycharts/commit/dc67756a72ce3061d655dac4e6754c86968e9f5f)]
+- fix table rendering of metadata flags [Miu Razvan - [`2c95cc8`](https://github.com/eea/volto-plotlycharts/commit/2c95cc86954c14ce5abcca2c8cffb53d32f7e11b)]
+- further improve lazy loading plotly [Miu Razvan - [`be744c9`](https://github.com/eea/volto-plotlycharts/commit/be744c922c7353fb95d507cfc5ef871b7754f0a1)]
+- fix metadata array [Dobricean Ioan Dorian - [`dbd3536`](https://github.com/eea/volto-plotlycharts/commit/dbd3536c2460ea4ee7ceb97850f212c03510143d)]
+- add specific height feature when 'autosize: true' is used [Miu Razvan - [`8458124`](https://github.com/eea/volto-plotlycharts/commit/84581248ef0bdfb9a173349c56c05584a9e37aab)]
+- Remove console.log [Tiberiu Ichim - [`91ec3b4`](https://github.com/eea/volto-plotlycharts/commit/91ec3b41c6e7b6b2752b484e0aa9b7ccd4399d7b)]
+- Fix injection [Tiberiu Ichim - [`f9d9568`](https://github.com/eea/volto-plotlycharts/commit/f9d956886a91ecb52acfcafd6aa43b16916c2958)]
+- Fix a var [Tiberiu Ichim - [`700fe34`](https://github.com/eea/volto-plotlycharts/commit/700fe341aec1cf0ebcdfd0b82b65f8867eb915c9)]
+- Lazy loading plotly [Tiberiu Ichim - [`0587300`](https://github.com/eea/volto-plotlycharts/commit/058730005fffcec12e5f25579640ce9065b5bb91)]
+- Lazy loading plotly [Tiberiu Ichim - [`ffed054`](https://github.com/eea/volto-plotlycharts/commit/ffed054854ac22af17738e0628296a05b6883dd9)]
+- Remove console.log [Tiberiu Ichim - [`8073564`](https://github.com/eea/volto-plotlycharts/commit/8073564a4698c1a961cd47989a6e332157f19910)]
+- WIP [Tiberiu Ichim - [`2a05efb`](https://github.com/eea/volto-plotlycharts/commit/2a05efb213c99d3d45e6648c777401ab450e9d2e)]
+- WIP [Tiberiu Ichim - [`e2e36b9`](https://github.com/eea/volto-plotlycharts/commit/e2e36b9e22e6498ac69befaae0fe34ee11de138c)]
+- WIP [Tiberiu Ichim - [`27ebe1d`](https://github.com/eea/volto-plotlycharts/commit/27ebe1d58a44f08a2f64ae3bc61c7580b4bdf4db)]
+- WIP on embedding data [Tiberiu Ichim - [`1363df0`](https://github.com/eea/volto-plotlycharts/commit/1363df0e6a358bcd8a819932a0877bd47fe92c91)]
### [13.0.3](https://github.com/eea/volto-plotlycharts/compare/13.0.2...13.0.3) - 18 August 2025
#### :bug: Bug Fixes
@@ -11,6 +39,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fix(filters-styles): only apply to top_filters [nileshgulia1 - [`7bd3078`](https://github.com/eea/volto-plotlycharts/commit/7bd30780b028052aed2bd2ac60269b19c2a3403e)]
- fix: width on visualization filters refs#288972 [nileshgulia1 - [`05e35be`](https://github.com/eea/volto-plotlycharts/commit/05e35be8d7ef7bafea5d7659d48c9971e7ef1191)]
+#### :hammer_and_wrench: Others
+
+- Merge pull request #165 from eea/develop [Nilesh - [`5403f05`](https://github.com/eea/volto-plotlycharts/commit/5403f05d34ab58da29bcd0835dfeb8a58d022bfa)]
### [13.0.2](https://github.com/eea/volto-plotlycharts/compare/13.0.1...13.0.2) - 28 July 2025
#### :house: Internal changes
diff --git a/package.json b/package.json
index 14178184..76236ddc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@eeacms/volto-plotlycharts",
- "version": "13.0.3",
+ "version": "13.0.4",
"description": "Plotly Charts and Editor integration for Volto",
"main": "src/index.js",
"author": "European Environment Agency: IDM2 A-Team",
@@ -25,7 +25,7 @@
"@eeacms/volto-datablocks"
],
"dependencies": {
- "@eeacms/react-chart-editor": "0.47.4",
+ "@eeacms/react-chart-editor": "0.47.6",
"@eeacms/volto-datablocks": "*",
"@eeacms/volto-embed": "*",
"@eeacms/volto-matomo": "*",
@@ -34,7 +34,6 @@
"jsoneditor": "10.2.0",
"jszip": "3.10.1",
"plotly.js": "^2.35.3",
- "react-plotly.js": "2.6.0",
"remixicon": "4.6.0"
},
"devDependencies": {
diff --git a/src/Blocks/Treemap/Treemap.jsx b/src/Blocks/Treemap/Treemap.jsx
index 66bbbb34..4f335fd4 100644
--- a/src/Blocks/Treemap/Treemap.jsx
+++ b/src/Blocks/Treemap/Treemap.jsx
@@ -9,11 +9,8 @@ import config from '@plone/volto/registry';
import { connectToProviderData } from '@eeacms/volto-datablocks/hocs';
import { VisibilitySensor } from '@eeacms/volto-datablocks/components';
-const LoadablePlotly = loadable(() =>
- import(
- /* webpackChunkName: "bise-react-plotly" */
- 'react-plotly.js'
- ),
+const PlotlyComponent = loadable(() =>
+ import('@eeacms/volto-plotlycharts/lib/react-plotly'),
);
/*
@@ -55,7 +52,7 @@ function Treemap(props) {
-
+
);
}
+function prepareEmbedData(dataSources, provider_metadata, core_metadata) {
+ let array = [];
+ Object.entries(dataSources).forEach(([key, items]) => {
+ items.forEach((item, index) => {
+ if (!array[index]) array[index] = {};
+ array[index][key] = item;
+ });
+ });
+
+ let readme = provider_metadata?.readme ? [provider_metadata?.readme] : [];
+ const metadataFlags = getMetadataFlags(core_metadata);
+ const metadataArrays = processMetadataArrays(core_metadata, metadataFlags);
+
+ return { array, readme, metadataArrays, metadataFlags };
+}
+
+function Table({ rows }) {
+ const stableKeys = Object.keys(rows?.[0] || {});
+
+ return (
+
+
+
+ {stableKeys.map((key) => (
+ | {key} |
+ ))}
+
+
+
+ {rows.map((row, index) => (
+
+ {stableKeys.map((key) => (
+ | {row[key]} |
+ ))}
+
+ ))}
+
+
+ );
+}
+
+function EmbedData(props) {
+ const { provider_metadata } = props; // reactChartEditorLib,
+ const { dataSources = {} } = props.data?.visualization || {};
+
+ const {
+ data_provenance,
+ other_organisations,
+ temporal_coverage,
+ publisher,
+ geo_coverage,
+ } = props.data?.properties || {};
+
+ const core_metadata = {
+ data_provenance: data_provenance?.data,
+ other_organisations,
+ temporal_coverage: temporal_coverage?.temporal,
+ publisher,
+ geo_coverage: geo_coverage?.geolocation,
+ };
+
+ const embedData = prepareEmbedData(
+ dataSources,
+ provider_metadata,
+ core_metadata,
+ );
+ const { array, readme, metadataArrays, metadataFlags } = embedData;
+
+ return (
+
+
Embed Data
+
+
+ {metadataFlags.hasDataProvenance && (
+ <>
+
Data Provenance
+
+ >
+ )}
+ {metadataFlags.hasOtherOrganisation && (
+ <>
+
Other Organisations
+
+ >
+ )}
+ {metadataFlags.hasTemporalCoverage && (
+ <>
+
Temporal Coverage
+
+ >
+ )}
+ {metadataFlags.hasGeoCoverage && (
+ <>
+
Geographical Coverage
+
+ >
+ )}
+ {metadataFlags.hasPublisher && (
+ <>
+
Publisher
+
+ >
+ )}
+
+
{readme}
+
+ );
+}
+
+const WithChartEditorLibEmbedData = injectLazyLibs(['reactChartEditorLib'])(
+ EmbedData,
+);
+
const ConnectedPlotlyComponent = compose(
connectBlockToVisualization(function getConfig(props) {
const url = flattenToAppURL(props.data?.vis_url);
@@ -323,12 +450,20 @@ const ConnectedPlotlyComponent = compose(
selfProvided:
props.data.visualization?.provider_url === props.data.properties?.['@id'],
})),
- injectLazyLibs(['reactSelect']),
+ injectLazyLibs(['reactSelect', 'reactChartEditor']),
)(UnconnectedPlotlyComponent);
export default function PlotlyComponent(props) {
return (
-
+ {
+ return (
+
+ );
+ }}
+ >
);
diff --git a/src/PlotlyEditor/PlotlyEditor.jsx b/src/PlotlyEditor/PlotlyEditor.jsx
index 35605a64..4e2ec3e3 100644
--- a/src/PlotlyEditor/PlotlyEditor.jsx
+++ b/src/PlotlyEditor/PlotlyEditor.jsx
@@ -9,8 +9,7 @@ import React, {
import { compose } from 'redux';
import { useLocation } from 'react-router-dom';
import { cloneDeep, isEqual, isNil, sortBy, debounce } from 'lodash';
-import DefaultPlotlyEditor, { constants } from '@eeacms/react-chart-editor';
-import plotly from 'plotly.js/dist/plotly-with-meta';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import { Api } from '@plone/volto/helpers';
@@ -46,6 +45,11 @@ const withValue = (WrappedComponent) => {
};
const UnconnectedPlotlyEditor = forwardRef((props, ref) => {
+ const { default: DefaultPlotlyEditor, constants } = props.reactChartEditor;
+ const plotly = props.plotlyLib?.default || props.plotlyLib;
+ const { TRACE_SRC_ATTRIBUTES, LAYOUT_SRC_ATTRIBUTES, EDITOR_ACTIONS } =
+ constants;
+
const update = useRef({});
const flags = useRef({});
const editor = useRef();
@@ -87,21 +91,21 @@ const UnconnectedPlotlyEditor = forwardRef((props, ref) => {
const updatedTrace = updateDataSources(
updateTrace(trace),
dataSources,
- constants.TRACE_SRC_ATTRIBUTES,
+ TRACE_SRC_ATTRIBUTES,
);
acc.push(updatedTrace);
return acc;
}, []);
- }, [value.data, dataSources]);
+ }, [value.data, dataSources, TRACE_SRC_ATTRIBUTES]);
const layout = useMemo(() => {
return updateDataSources(
value.layout || {},
dataSources,
- constants.LAYOUT_SRC_ATTRIBUTES,
+ LAYOUT_SRC_ATTRIBUTES,
);
- }, [value.layout, dataSources]);
+ }, [value.layout, dataSources, LAYOUT_SRC_ATTRIBUTES]);
const ctx = useMemo(
() => ({
@@ -192,7 +196,7 @@ const UnconnectedPlotlyEditor = forwardRef((props, ref) => {
function updateTemplate(template) {
editor.current.onUpdate({
- type: constants.EDITOR_ACTIONS.UPDATE_LAYOUT,
+ type: EDITOR_ACTIONS.UPDATE_LAYOUT,
payload: {
update: {
template,
@@ -213,7 +217,7 @@ const UnconnectedPlotlyEditor = forwardRef((props, ref) => {
updateTemplate(theme);
}
}
- }, [initialized, themes, layout.template]);
+ }, [initialized, themes, layout.template, EDITOR_ACTIONS.UPDATE_LAYOUT]);
// Clean up
@@ -297,6 +301,7 @@ const UnconnectedPlotlyEditor = forwardRef((props, ref) => {
});
const ConnectedPlotlyEditor = compose(
+ injectLazyLibs(['reactChartEditor', 'plotlyLib']),
withValue,
connectToProviderData((props) => ({
provider_url: props.value.provider_url,
diff --git a/src/PlotlyEditor/panels/GraphFilterPanel.jsx b/src/PlotlyEditor/panels/GraphFilterPanel.jsx
index dadcdb45..8ceec6ca 100644
--- a/src/PlotlyEditor/panels/GraphFilterPanel.jsx
+++ b/src/PlotlyEditor/panels/GraphFilterPanel.jsx
@@ -1,14 +1,16 @@
import PropTypes from 'prop-types';
import { isArray, uniq } from 'lodash';
-// Containers
+
import PlotlyPanel from '@eeacms/react-chart-editor/lib/components/containers/PlotlyPanel';
import PlotlyFold from '@eeacms/react-chart-editor/lib/components/containers/PlotlyFold';
-// Widgets
import Dropdown from '@eeacms/react-chart-editor/lib/components/widgets/Dropdown';
import TextInput from '@eeacms/react-chart-editor/lib/components/widgets/TextInput';
-// Field
import Field from '@eeacms/react-chart-editor/lib/components/fields/Field';
+// Containers
+// Widgets
+// Field
+
function getFilterOptions(rows) {
if (!isArray(rows)) return [];
return uniq(rows).map((value) => ({
diff --git a/src/PlotlyEditor/panels/StyleLayoutPanel.jsx b/src/PlotlyEditor/panels/StyleLayoutPanel.jsx
index 47f1cf8d..cda3af7c 100644
--- a/src/PlotlyEditor/panels/StyleLayoutPanel.jsx
+++ b/src/PlotlyEditor/panels/StyleLayoutPanel.jsx
@@ -18,7 +18,10 @@ import {
Radio,
Info,
} from '@eeacms/react-chart-editor';
-import { HoverColor } from '@eeacms/react-chart-editor/lib/components/fields/derived';
+import {
+ HoverColor,
+ NumericHeight,
+} from '@eeacms/react-chart-editor/lib/components/fields/derived';
import DataSelector from '@eeacms/react-chart-editor/lib/components/fields/DataSelector';
import { TextInput } from '../fields';
@@ -253,6 +256,7 @@ const StyleLayoutPanel = (props, { localize: _ }) => (
+
diff --git a/src/PlotlyEditor/widgets/TemplateSelector.jsx b/src/PlotlyEditor/widgets/TemplateSelector.jsx
index 24bd9d39..b4bdf965 100644
--- a/src/PlotlyEditor/widgets/TemplateSelector.jsx
+++ b/src/PlotlyEditor/widgets/TemplateSelector.jsx
@@ -1,9 +1,11 @@
import PropTypes from 'prop-types';
import { omit, sortBy } from 'lodash';
-import { renderTraceIcon } from '@eeacms/react-chart-editor';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+// import { renderTraceIcon } from '@eeacms/react-chart-editor';
function Template(props) {
const { type, label, onSelect } = props;
+ const { renderTraceIcon } = props.reactChartEditorLib;
const ComplexIcon = renderTraceIcon(type.icon || type.value, 'TraceType');
@@ -99,5 +101,8 @@ function TemplateSelector(props) {
TemplateSelector.contextTypes = {
ctx: PropTypes.object,
};
+const InjectedTemplateSelector = injectLazyLibs(['react-chart-editor'])(
+ TemplateSelector,
+);
-export default TemplateSelector;
+export default InjectedTemplateSelector;
diff --git a/src/Utils/Download.jsx b/src/Utils/Download.jsx
index 65bde840..1ef8501d 100644
--- a/src/Utils/Download.jsx
+++ b/src/Utils/Download.jsx
@@ -1,24 +1,20 @@
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import React from 'react';
import cx from 'classnames';
import { cloneDeep } from 'lodash';
-import loadable from '@loadable/component';
import { useLocation } from 'react-router-dom';
import { Popup } from 'semantic-ui-react';
import { toPublicURL } from '@plone/volto/helpers';
import {
- convertToCSV,
exportCSVFile,
exportZipFile,
groupDataByDataset,
- processTraceData,
- spreadCoreMetadata,
} from '@eeacms/volto-plotlycharts/helpers/csvString';
+import { generateOriginalCSV, generateCSVForDataset } from './utils';
-const Plotly = loadable.lib(() => import('plotly.js/dist/plotly.min'));
-
-export default function Download(props) {
+function Download(props) {
const location = useLocation();
- const { chartData, filters, provider_metadata } = props;
+ const { chartData, filters, provider_metadata, reactChartEditorLib } = props;
const url_source = toPublicURL(location.pathname);
@@ -43,111 +39,6 @@ export default function Download(props) {
const [open, setOpen] = React.useState(false);
- const getMetadataFlags = () => ({
- hasDataProvenance: core_metadata.data_provenance?.length > 0,
- hasOtherOrganisation: core_metadata.other_organisations?.length > 0,
- hasTemporalCoverage: core_metadata.temporal_coverage?.length > 0,
- hasGeoCoverage: core_metadata.geo_coverage?.length > 0,
- hasPublisher: core_metadata.publisher?.length > 0,
- });
-
- const processMetadataArrays = (metadataFlags) => {
- const arrays = {
- data_provenance_array: [],
- other_organisation_array: [],
- temporal_coverage_array: [],
- geo_coverage_array: [],
- publisher_array: [],
- };
-
- if (
- metadataFlags.hasDataProvenance ||
- metadataFlags.hasOtherOrganisation ||
- metadataFlags.hasTemporalCoverage ||
- metadataFlags.hasGeoCoverage ||
- metadataFlags.hasPublisher
- ) {
- Object.entries(spreadCoreMetadata(core_metadata)).forEach(
- ([key, items]) => {
- items.forEach((item, index) => {
- if (key.includes('data_provenance') || key.includes('Sources')) {
- if (!arrays.data_provenance_array[index])
- arrays.data_provenance_array[index] = {};
- arrays.data_provenance_array[index][key] = item;
- }
- if (
- key.includes('other_organisation') ||
- key.includes('Other organisations involved')
- ) {
- if (!arrays.other_organisation_array[index])
- arrays.other_organisation_array[index] = {};
- arrays.other_organisation_array[index][key] = item;
- }
- if (
- key.includes('temporal_coverage') ||
- key.includes('Temporal coverage')
- ) {
- if (!arrays.temporal_coverage_array[index])
- arrays.temporal_coverage_array[index] = {};
- arrays.temporal_coverage_array[index][key] = item;
- }
- if (
- key.includes('geo_coverage') ||
- key.includes('Geographical coverage')
- ) {
- if (!arrays.geo_coverage_array[index])
- arrays.geo_coverage_array[index] = {};
- arrays.geo_coverage_array[index][key] = item;
- }
- if (key.includes('publisher') || key.includes('Publisher')) {
- if (!arrays.publisher_array[index])
- arrays.publisher_array[index] = {};
- arrays.publisher_array[index][key] = item;
- }
- });
- },
- );
- }
-
- return arrays;
- };
-
- const generateFinalCSV = (array, readme, metadataFlags, metadataArrays) => {
- const data_csv = convertToCSV(array, readme);
-
- const data_provenance_csv = metadataFlags.hasDataProvenance
- ? convertToCSV(metadataArrays.data_provenance_array, [], true)
- : '';
- const other_organisation_csv = metadataFlags.hasOtherOrganisation
- ? convertToCSV(metadataArrays.other_organisation_array, [], true)
- : '';
- const temporal_coverage_csv = metadataFlags.hasTemporalCoverage
- ? convertToCSV(metadataArrays.temporal_coverage_array, [], true)
- : '';
- const geo_coverage_csv = metadataFlags.hasGeoCoverage
- ? convertToCSV(metadataArrays.geo_coverage_array, [], true)
- : '';
- const publisher_csv = metadataFlags.hasPublisher
- ? convertToCSV(metadataArrays.publisher_array, [], true)
- : '';
-
- const download_source_csv = convertToCSV(
- [{ 'Downloaded from: ': url_source }],
- [],
- true,
- );
-
- return (
- download_source_csv +
- publisher_csv +
- other_organisation_csv +
- data_provenance_csv +
- geo_coverage_csv +
- temporal_coverage_csv +
- data_csv
- );
- };
-
const handleDownloadData = async () => {
const datasets = groupDataByDataset(chartData);
@@ -170,13 +61,25 @@ export default function Download(props) {
// Add individual CSV files for each dataset
for (const [, datasetData] of Object.entries(datasets)) {
- const csvData = await generateCSVForDataset(datasetData);
+ const csvData = generateCSVForDataset(
+ dataSources,
+ datasetData,
+ provider_metadata,
+ core_metadata,
+ url_source,
+ reactChartEditorLib,
+ );
const fileName = `${datasetData.name || 'data'}.csv`;
zip.file(fileName, csvData);
}
// Add the complete CSV with all data
- const completeCSVData = generateOriginalCSV();
+ const completeCSVData = generateOriginalCSV(
+ dataSources,
+ provider_metadata,
+ url_source,
+ core_metadata,
+ );
zip.file(`${title}.csv`, completeCSVData);
const zipBlob = await zip.generateAsync({ type: 'blob' });
@@ -187,77 +90,18 @@ export default function Download(props) {
}
};
- const generateCSVForDataset = async (datasetData) => {
- let array = [];
- let readme = provider_metadata?.readme ? [provider_metadata?.readme] : [];
-
- // Collect all trace data separately
- const allTraceData = [];
- for (const trace of datasetData.data) {
- const traceData = await processTraceData(trace, dataSources);
- allTraceData.push(traceData);
- }
-
- // If no processed data from traces, fall back to original data sources
- if (
- allTraceData.length === 0 ||
- allTraceData.every((data) => data.length === 0)
- ) {
- Object.entries(dataSources).forEach(([key, items]) => {
- items.forEach((item, index) => {
- if (!array[index]) array[index] = {};
- array[index][key] = item;
- });
- });
- } else {
- const maxLength = Math.max(...allTraceData.map((data) => data.length));
-
- for (let i = 0; i < maxLength; i++) {
- if (!array[i]) array[i] = {};
-
- allTraceData.forEach((traceData) => {
- if (traceData[i]) {
- Object.keys(traceData[i]).forEach((key) => {
- if (
- traceData[i][key] !== null &&
- traceData[i][key] !== undefined
- ) {
- array[i][key] = traceData[i][key];
- }
- });
- }
- });
- }
- }
-
- const metadataFlags = getMetadataFlags();
- const metadataArrays = processMetadataArrays(metadataFlags);
- return generateFinalCSV(array, readme, metadataFlags, metadataArrays);
- };
-
- const generateOriginalCSV = () => {
- let array = [];
- let readme = provider_metadata?.readme ? [provider_metadata?.readme] : [];
-
- Object.entries(dataSources).forEach(([key, items]) => {
- items.forEach((item, index) => {
- if (!array[index]) array[index] = {};
- array[index][key] = item;
- });
- });
-
- const metadataFlags = getMetadataFlags();
- const metadataArrays = processMetadataArrays(metadataFlags);
- return generateFinalCSV(array, readme, metadataFlags, metadataArrays);
- };
-
const handleDownloadSingleCSV = () => {
- const csvData = generateOriginalCSV();
+ const csvData = generateOriginalCSV(
+ dataSources,
+ provider_metadata,
+ url_source,
+ core_metadata,
+ );
exportCSVFile(csvData, title);
};
const handleDownloadImage = async (type) => {
- const plotly = (await Plotly.load()).default;
+ const plotly = props.plotlyLib?.default || props.plotlyLib;
const container = document.createElement('div');
container.id = '__plotly_download_container__';
@@ -458,3 +302,8 @@ export default function Download(props) {
/>
);
}
+
+const WithLibsDownload = injectLazyLibs(['reactChartEditorLib', 'plotlyLib'])(
+ Download,
+);
+export default WithLibsDownload;
diff --git a/src/Utils/utils.js b/src/Utils/utils.js
new file mode 100644
index 00000000..82d11d34
--- /dev/null
+++ b/src/Utils/utils.js
@@ -0,0 +1,203 @@
+import {
+ convertToCSV,
+ processTraceData,
+ spreadCoreMetadata,
+} from '@eeacms/volto-plotlycharts/helpers/csvString';
+
+export const generateFinalCSV = (
+ array,
+ readme,
+ metadataFlags,
+ metadataArrays,
+ url_source,
+) => {
+ const data_csv = convertToCSV(array, readme);
+
+ const data_provenance_csv = metadataFlags.hasDataProvenance
+ ? convertToCSV(metadataArrays.data_provenance_array, [], true)
+ : '';
+ const other_organisation_csv = metadataFlags.hasOtherOrganisation
+ ? convertToCSV(metadataArrays.other_organisation_array, [], true)
+ : '';
+ const temporal_coverage_csv = metadataFlags.hasTemporalCoverage
+ ? convertToCSV(metadataArrays.temporal_coverage_array, [], true)
+ : '';
+ const geo_coverage_csv = metadataFlags.hasGeoCoverage
+ ? convertToCSV(metadataArrays.geo_coverage_array, [], true)
+ : '';
+ const publisher_csv = metadataFlags.hasPublisher
+ ? convertToCSV(metadataArrays.publisher_array, [], true)
+ : '';
+
+ const download_source_csv = convertToCSV(
+ [{ 'Downloaded from: ': url_source }],
+ [],
+ true,
+ );
+
+ return (
+ download_source_csv +
+ publisher_csv +
+ other_organisation_csv +
+ data_provenance_csv +
+ geo_coverage_csv +
+ temporal_coverage_csv +
+ data_csv
+ );
+};
+
+export const generateOriginalCSV = (
+ dataSources,
+ provider_metadata,
+ url_source,
+ core_metadata,
+) => {
+ let array = [];
+ let readme = provider_metadata?.readme ? [provider_metadata?.readme] : [];
+
+ Object.entries(dataSources).forEach(([key, items]) => {
+ items.forEach((item, index) => {
+ if (!array[index]) array[index] = {};
+ array[index][key] = item;
+ });
+ });
+
+ const metadataFlags = getMetadataFlags(core_metadata);
+ const metadataArrays = processMetadataArrays(core_metadata, metadataFlags);
+
+ return generateFinalCSV(
+ array,
+ readme,
+ metadataFlags,
+ metadataArrays,
+ url_source,
+ );
+};
+
+export const getMetadataFlags = (core_metadata) => ({
+ hasDataProvenance: core_metadata.data_provenance?.length > 0,
+ hasOtherOrganisation: core_metadata.other_organisations?.length > 0,
+ hasTemporalCoverage: core_metadata.temporal_coverage?.length > 0,
+ hasGeoCoverage: core_metadata.geo_coverage?.length > 0,
+ hasPublisher: core_metadata.publisher?.length > 0,
+});
+
+export const processMetadataArrays = (core_metadata, metadataFlags) => {
+ const arrays = {
+ data_provenance_array: [],
+ other_organisation_array: [],
+ temporal_coverage_array: [],
+ geo_coverage_array: [],
+ publisher_array: [],
+ };
+
+ if (
+ metadataFlags.hasDataProvenance ||
+ metadataFlags.hasOtherOrganisation ||
+ metadataFlags.hasTemporalCoverage ||
+ metadataFlags.hasGeoCoverage ||
+ metadataFlags.hasPublisher
+ ) {
+ Object.entries(spreadCoreMetadata(core_metadata)).forEach(
+ ([key, items]) => {
+ items.forEach((item, index) => {
+ if (key.includes('data_provenance') || key.includes('Sources')) {
+ if (!arrays.data_provenance_array[index])
+ arrays.data_provenance_array[index] = {};
+ arrays.data_provenance_array[index][key] = item;
+ }
+ if (
+ key.includes('other_organisation') ||
+ key.includes('Other organisations involved')
+ ) {
+ if (!arrays.other_organisation_array[index])
+ arrays.other_organisation_array[index] = {};
+ arrays.other_organisation_array[index][key] = item;
+ }
+ if (
+ key.includes('temporal_coverage') ||
+ key.includes('Temporal coverage')
+ ) {
+ if (!arrays.temporal_coverage_array[index])
+ arrays.temporal_coverage_array[index] = {};
+ arrays.temporal_coverage_array[index][key] = item;
+ }
+ if (
+ key.includes('geo_coverage') ||
+ key.includes('Geographical coverage')
+ ) {
+ if (!arrays.geo_coverage_array[index])
+ arrays.geo_coverage_array[index] = {};
+ arrays.geo_coverage_array[index][key] = item;
+ }
+ if (key.includes('publisher') || key.includes('Publisher')) {
+ if (!arrays.publisher_array[index])
+ arrays.publisher_array[index] = {};
+ arrays.publisher_array[index][key] = item;
+ }
+ });
+ },
+ );
+ }
+
+ return arrays;
+};
+
+export const generateCSVForDataset = (
+ dataSources,
+ datasetData,
+ provider_metadata,
+ core_metadata,
+ url_source,
+ reactChartEditorLib,
+) => {
+ let array = [];
+ let readme = provider_metadata?.readme ? [provider_metadata?.readme] : [];
+
+ // Collect all trace data separately
+ const allTraceData = [];
+ for (const trace of datasetData.data) {
+ const traceData = processTraceData(trace, dataSources, reactChartEditorLib);
+ allTraceData.push(traceData);
+ }
+
+ // If no processed data from traces, fall back to original data sources
+ if (
+ allTraceData.length === 0 ||
+ allTraceData.every((data) => data.length === 0)
+ ) {
+ Object.entries(dataSources).forEach(([key, items]) => {
+ items.forEach((item, index) => {
+ if (!array[index]) array[index] = {};
+ array[index][key] = item;
+ });
+ });
+ } else {
+ const maxLength = Math.max(...allTraceData.map((data) => data.length));
+
+ for (let i = 0; i < maxLength; i++) {
+ if (!array[i]) array[i] = {};
+
+ allTraceData.forEach((traceData) => {
+ if (traceData[i]) {
+ Object.keys(traceData[i]).forEach((key) => {
+ if (traceData[i][key] !== null && traceData[i][key] !== undefined) {
+ array[i][key] = traceData[i][key];
+ }
+ });
+ }
+ });
+ }
+ }
+
+ const metadataFlags = getMetadataFlags(core_metadata);
+ const metadataArrays = processMetadataArrays(core_metadata, metadataFlags);
+
+ return generateFinalCSV(
+ array,
+ readme,
+ metadataFlags,
+ metadataArrays,
+ url_source,
+ );
+};
diff --git a/src/Widgets/TemplatesWidget.jsx b/src/Widgets/TemplatesWidget.jsx
index 9615c787..a2e768af 100644
--- a/src/Widgets/TemplatesWidget.jsx
+++ b/src/Widgets/TemplatesWidget.jsx
@@ -1,3 +1,4 @@
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
import { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { sortBy, omit } from 'lodash';
@@ -15,9 +16,9 @@ const plotlyUtils = loadable.lib(() =>
import('@eeacms/volto-plotlycharts/helpers/plotly'),
);
-const renderTraceIcon = __CLIENT__
- ? require('@eeacms/react-chart-editor').renderTraceIcon
- : () => null;
+// const renderTraceIcon = __CLIENT__
+// ? require('@eeacms/react-chart-editor').renderTraceIcon
+// : () => null;
const EditTemplate = (props) => {
const [fadeInOut, setFadeInOut] = useState(true);
@@ -55,11 +56,19 @@ const EditTemplate = (props) => {
);
};
-const Template = ({ type, label, onEdit, onDelete, onClone }) => {
+const Template = ({
+ type,
+ label,
+ onEdit,
+ onDelete,
+ onClone,
+ reactChartEditor,
+}) => {
+ const { renderTraceIcon } = reactChartEditor;
const [open, setOpen] = useState(false);
const ComplexIcon = useMemo(
() => renderTraceIcon(type.icon || type.value, 'TraceType'),
- [type],
+ [type, renderTraceIcon],
);
return (
@@ -266,4 +275,4 @@ TemplatesWidget.contextTypes = {
formData: PropTypes.object,
};
-export default TemplatesWidget;
+export default injectLazyLibs(['reactChartEditor'])(TemplatesWidget);
diff --git a/src/helpers/csvString.js b/src/helpers/csvString.js
index 54a92724..8da36880 100644
--- a/src/helpers/csvString.js
+++ b/src/helpers/csvString.js
@@ -233,15 +233,13 @@ function groupDataByDataset(chartData) {
return datasets;
}
-async function processTraceData(trace, dataSources) {
+function processTraceData(trace, dataSources, reactChartEditorLib) {
let processedData = [];
// Collect all columns used by the trace
const usedColumns = new Set();
- const { getAttrsPath, constants, getSrcAttr } = await import(
- '@eeacms/react-chart-editor/lib'
- );
+ const { getAttrsPath, constants, getSrcAttr } = reactChartEditorLib;
// Get all data attributes from constants.TRACE_SRC_ATTRIBUTES
const traceDataAttrs = getAttrsPath(trace, constants.TRACE_SRC_ATTRIBUTES);
diff --git a/src/helpers/editor.js b/src/helpers/editor.js
index 8300041a..8c356ab7 100644
--- a/src/helpers/editor.js
+++ b/src/helpers/editor.js
@@ -5,7 +5,7 @@ import { Toast } from '@plone/volto/components';
import loadable from '@loadable/component';
const LoadableJsonEditor = loadable.lib(() =>
- import('jsoneditor/dist/jsoneditor'),
+ import('jsoneditor/dist/jsoneditor.min'),
);
const jsoneditor = __CLIENT__ && LoadableJsonEditor;
diff --git a/src/index.js b/src/index.js
index 131c4add..a94311b3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,5 @@
+import loadable from '@loadable/component';
+
import installBlocks from './Blocks';
import { VisualizationView } from './Views';
import {
@@ -63,6 +65,15 @@ const applyConfig = (config) => {
},
];
+ config.settings.loadables = {
+ ...config.settings.loadables,
+ reactChartEditor: loadable.lib(() => import('@eeacms/react-chart-editor')),
+ reactChartEditorLib: loadable.lib(() =>
+ import('@eeacms/react-chart-editor/lib'),
+ ),
+ plotlyLib: loadable.lib(() => import('plotly.js/dist/plotly-with-meta')),
+ };
+
return [installBlocks].reduce((acc, apply) => apply(acc), config);
};
diff --git a/src/less/plotly.less b/src/less/plotly.less
index 9dfc1a13..732639c3 100644
--- a/src/less/plotly.less
+++ b/src/less/plotly.less
@@ -8,10 +8,29 @@
overflow-x: auto;
}
+.visualization-wrapper {
+ position: relative;
+
+ &.loading {
+ height: 80px;
+ }
+
+ .plotly-placeholder {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ display: flex;
+ justify-content: center;
+ padding: '1rem 0';
+ transform: translate(-50%, -50%);
+ }
+}
+
.plotly-component {
margin-bottom: 1rem;
- .svg-container {
+ .visualization,
+ .js-plotly-plot {
height: var(--svg-container-height) !important;
}
diff --git a/src/lib/react-plotly.js b/src/lib/react-plotly.js
new file mode 100644
index 00000000..2add73c0
--- /dev/null
+++ b/src/lib/react-plotly.js
@@ -0,0 +1,301 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+
+// The naming convention is:
+// - events are attached as `'plotly_' + eventName.toLowerCase()`
+// - react props are `'on' + eventName`
+const eventNames = [
+ 'AfterExport',
+ 'AfterPlot',
+ 'Animated',
+ 'AnimatingFrame',
+ 'AnimationInterrupted',
+ 'AutoSize',
+ 'BeforeExport',
+ 'BeforeHover',
+ 'ButtonClicked',
+ 'Click',
+ 'ClickAnnotation',
+ 'Deselect',
+ 'DoubleClick',
+ 'Framework',
+ 'Hover',
+ 'LegendClick',
+ 'LegendDoubleClick',
+ 'Relayout',
+ 'Relayouting',
+ 'Restyle',
+ 'Redraw',
+ 'Selected',
+ 'Selecting',
+ 'SliderChange',
+ 'SliderEnd',
+ 'SliderStart',
+ 'SunburstClick',
+ 'Transitioning',
+ 'TransitionInterrupted',
+ 'Unhover',
+ 'WebGlContextLost',
+];
+
+const updateEvents = [
+ 'plotly_restyle',
+ 'plotly_redraw',
+ 'plotly_relayout',
+ 'plotly_relayouting',
+ 'plotly_doubleclick',
+ 'plotly_animated',
+ 'plotly_sunburstclick',
+];
+
+// Check if a window is available since SSR (server-side rendering)
+// breaks unnecessarily if you try to use it server-side.
+const isBrowser = typeof window !== 'undefined';
+
+class PlotlyComponent extends Component {
+ constructor(props) {
+ super(props);
+
+ this.p = Promise.resolve();
+ this.resizeHandler = null;
+ this.handlers = {};
+
+ this.syncWindowResize = this.syncWindowResize.bind(this);
+ this.syncEventHandlers = this.syncEventHandlers.bind(this);
+ this.attachUpdateEvents = this.attachUpdateEvents.bind(this);
+ this.getRef = this.getRef.bind(this);
+ this.handleUpdate = this.handleUpdate.bind(this);
+ this.figureCallback = this.figureCallback.bind(this);
+ this.updatePlotly = this.updatePlotly.bind(this);
+ this.plotly = props.plotlyLib?.default || props.plotlyLib;
+ }
+
+ updatePlotly(
+ shouldInvokeResizeHandler,
+ figureCallbackFunction,
+ shouldAttachUpdateEvents,
+ ) {
+ this.p = this.p
+ .then(() => {
+ if (this.unmounting) {
+ return;
+ }
+ if (!this.el) {
+ throw new Error('Missing element reference');
+ }
+ // eslint-disable-next-line consistent-return
+ return this.plotly.react(this.el, {
+ data: this.props.data,
+ layout: this.props.layout,
+ config: this.props.config,
+ frames: this.props.frames,
+ });
+ })
+ .then(() => {
+ if (this.unmounting) {
+ return;
+ }
+ this.syncWindowResize(shouldInvokeResizeHandler);
+ this.syncEventHandlers();
+ this.figureCallback(figureCallbackFunction);
+ if (shouldAttachUpdateEvents) {
+ this.attachUpdateEvents();
+ }
+ })
+ .catch((err) => {
+ if (this.props.onError) {
+ this.props.onError(err);
+ }
+ });
+ }
+
+ componentDidMount() {
+ this.unmounting = false;
+
+ this.updatePlotly(true, this.props.onInitialized, true);
+ }
+
+ componentDidUpdate(prevProps) {
+ this.unmounting = false;
+
+ // frames *always* changes identity so fall back to check length only :(
+ const numPrevFrames =
+ prevProps.frames && prevProps.frames.length ? prevProps.frames.length : 0;
+ const numNextFrames =
+ this.props.frames && this.props.frames.length
+ ? this.props.frames.length
+ : 0;
+
+ const figureChanged = !(
+ prevProps.layout === this.props.layout &&
+ prevProps.data === this.props.data &&
+ prevProps.config === this.props.config &&
+ numNextFrames === numPrevFrames
+ );
+ const revisionDefined = prevProps.revision !== void 0;
+ const revisionChanged = prevProps.revision !== this.props.revision;
+
+ if (
+ !figureChanged &&
+ (!revisionDefined || (revisionDefined && !revisionChanged))
+ ) {
+ return;
+ }
+
+ this.updatePlotly(false, this.props.onUpdate, false);
+ }
+
+ componentWillUnmount() {
+ this.unmounting = true;
+
+ this.figureCallback(this.props.onPurge);
+
+ if (this.resizeHandler && isBrowser) {
+ window.removeEventListener('resize', this.resizeHandler);
+ this.resizeHandler = null;
+ }
+
+ this.removeUpdateEvents();
+
+ this.plotly.purge(this.el);
+ }
+
+ attachUpdateEvents() {
+ if (!this.el || !this.el.removeListener) {
+ return;
+ }
+
+ updateEvents.forEach((updateEvent) => {
+ this.el.on(updateEvent, this.handleUpdate);
+ });
+ }
+
+ removeUpdateEvents() {
+ if (!this.el || !this.el.removeListener) {
+ return;
+ }
+
+ updateEvents.forEach((updateEvent) => {
+ this.el.removeListener(updateEvent, this.handleUpdate);
+ });
+ }
+
+ handleUpdate() {
+ this.figureCallback(this.props.onUpdate);
+ }
+
+ figureCallback(callback) {
+ if (typeof callback === 'function') {
+ const { data, layout } = this.el;
+ const frames = this.el._transitionData
+ ? this.el._transitionData._frames
+ : null;
+ const figure = { data, layout, frames };
+ callback(figure, this.el);
+ }
+ }
+
+ syncWindowResize(invoke) {
+ if (!isBrowser) {
+ return;
+ }
+
+ if (this.props.useResizeHandler && !this.resizeHandler) {
+ this.resizeHandler = () => this.plotly.Plots.resize(this.el);
+ window.addEventListener('resize', this.resizeHandler);
+ if (invoke) {
+ this.resizeHandler();
+ }
+ } else if (!this.props.useResizeHandler && this.resizeHandler) {
+ window.removeEventListener('resize', this.resizeHandler);
+ this.resizeHandler = null;
+ }
+ }
+
+ getRef(el) {
+ this.el = el;
+
+ if (this.props.debug && isBrowser) {
+ window.gd = this.el;
+ }
+ }
+
+ // Attach and remove event handlers as they're added or removed from props:
+ syncEventHandlers() {
+ eventNames.forEach((eventName) => {
+ const prop = this.props['on' + eventName];
+ const handler = this.handlers[eventName];
+ const hasHandler = Boolean(handler);
+
+ if (prop && !hasHandler) {
+ this.addEventHandler(eventName, prop);
+ } else if (!prop && hasHandler) {
+ // Needs to be removed:
+ this.removeEventHandler(eventName);
+ } else if (prop && hasHandler && prop !== handler) {
+ // replace the handler
+ this.removeEventHandler(eventName);
+ this.addEventHandler(eventName, prop);
+ }
+ });
+ }
+
+ addEventHandler(eventName, prop) {
+ this.handlers[eventName] = prop;
+ this.el.on(this.getPlotlyEventName(eventName), this.handlers[eventName]);
+ }
+
+ removeEventHandler(eventName) {
+ this.el.removeListener(
+ this.getPlotlyEventName(eventName),
+ this.handlers[eventName],
+ );
+ delete this.handlers[eventName];
+ }
+
+ getPlotlyEventName(eventName) {
+ return 'plotly_' + eventName.toLowerCase();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+PlotlyComponent.propTypes = {
+ data: PropTypes.arrayOf(PropTypes.object),
+ config: PropTypes.object,
+ layout: PropTypes.object,
+ frames: PropTypes.arrayOf(PropTypes.object),
+ revision: PropTypes.number,
+ onInitialized: PropTypes.func,
+ onPurge: PropTypes.func,
+ onError: PropTypes.func,
+ onUpdate: PropTypes.func,
+ debug: PropTypes.bool,
+ style: PropTypes.object,
+ className: PropTypes.string,
+ useResizeHandler: PropTypes.bool,
+ divId: PropTypes.string,
+};
+
+eventNames.forEach((eventName) => {
+ PlotlyComponent.propTypes['on' + eventName] = PropTypes.func; // eslint-disable-line
+});
+
+PlotlyComponent.defaultProps = {
+ debug: false,
+ useResizeHandler: false,
+ data: [],
+ style: { position: 'relative', display: 'inline-block' },
+};
+
+export default injectLazyLibs(['plotlyLib'])(PlotlyComponent);