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) {
- +
import('react-plotly.js')); +const PlotlyComponent = loadable(() => + import('@eeacms/volto-plotlycharts/lib/react-plotly'), +); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); diff --git a/src/PlotlyComponent/PlotlyComponent.jsx b/src/PlotlyComponent/PlotlyComponent.jsx index 8678b3ec..c98073a0 100644 --- a/src/PlotlyComponent/PlotlyComponent.jsx +++ b/src/PlotlyComponent/PlotlyComponent.jsx @@ -4,7 +4,7 @@ import { connect } from 'react-redux'; import { mapKeys, isArray, uniqBy, sortBy, isNil } from 'lodash'; import cx from 'classnames'; import { FormField } from 'semantic-ui-react'; -import { constants } from '@eeacms/react-chart-editor'; +// import { constants } from '@eeacms/react-chart-editor'; import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; import { flattenToAppURL } from '@plone/volto/helpers'; import { VisibilitySensor } from '@eeacms/volto-datablocks/components'; @@ -21,6 +21,13 @@ import { import { Toolbar } from '@eeacms/volto-plotlycharts/Utils'; import Plot from './Plot'; import Placeholder from './Placeholder'; +import { + getMetadataFlags, + processMetadataArrays, +} from '@eeacms/volto-plotlycharts/Utils/utils'; + +// generateCSVForDataset, +// generateOriginalCSV, function getFilterOptions(rows, rowsOrder = null) { if (!isArray(rows)) return []; @@ -38,6 +45,8 @@ function getFilterOptions(rows, rowsOrder = null) { } function UnconnectedPlotlyComponent(props) { + const { reactChartEditor } = props; + const { constants } = reactChartEditor; const container = useRef(); const el = useRef(); const Select = props.reactSelect.default; @@ -121,7 +130,7 @@ function UnconnectedPlotlyComponent(props) { acc.push(updatedTrace); return acc; }, []); - }, [value.data, dataSources, filters]); + }, [value.data, dataSources, filters, constants.TRACE_SRC_ATTRIBUTES]); const layout = useMemo(() => { return updateDataSources( @@ -129,7 +138,7 @@ function UnconnectedPlotlyComponent(props) { dataSources, constants.LAYOUT_SRC_ATTRIBUTES, ); - }, [value.layout, dataSources]); + }, [value.layout, dataSources, constants.LAYOUT_SRC_ATTRIBUTES]); const toolbarData = useMemo(() => { return { @@ -228,9 +237,6 @@ function UnconnectedPlotlyComponent(props) { mobile, })} > - {(loadingVisualization || loadingProviderData || !initialized) && ( - - )} {initialized && filters.length > 0 && (
{filters.map((filter, index) => { @@ -260,30 +266,32 @@ function UnconnectedPlotlyComponent(props) { })}
)} - {!loadingProviderData && ( -
- -
- )} +
+ {(loadingVisualization || loadingProviderData || !initialized) && ( + + )} + {!loadingProviderData && ( +
+ +
+ )} +
{initialized && ( } /> )} + +
); } +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) => ( + + ))} + + + + {rows.map((row, index) => ( + + {stableKeys.map((key) => ( + + ))} + + ))} + +
{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);