diff --git a/server/storage/test.sqlite b/server/storage/test.sqlite index a09c885d5..d3b77d81a 100644 Binary files a/server/storage/test.sqlite and b/server/storage/test.sqlite differ diff --git a/vis/js/actions/index.js b/vis/js/actions/index.js index 0f2e16af2..46e430c79 100644 --- a/vis/js/actions/index.js +++ b/vis/js/actions/index.js @@ -60,27 +60,33 @@ export const preinitializeStore = (configObject) => ({ * Action for initializing the data that aren't known in advance. * @param {Object} configObject the default_config.json + data_config.json * @param {Object} contextObject the app context - * @param {Array} dataArray the papers data + * @param {Array} papers the papers data array + * @param {Array} areas the areas data array + * @param {Array} streams the streams data array */ export const initializeStore = ( configObject, contextObject, - dataArray, - streamData, + papers, + areas, + streams, chartSize, streamWidth, streamHeight, - listHeight + listHeight, + scalingFactors ) => ({ type: "INITIALIZE", configObject, contextObject, - dataArray, - streamData, + papers, + areas, + streams, chartSize, streamWidth, streamHeight, listHeight, + scalingFactors, }); /** diff --git a/vis/js/components/Streamgraph.js b/vis/js/components/Streamgraph.js index 27e10b192..d42f52848 100644 --- a/vis/js/components/Streamgraph.js +++ b/vis/js/components/Streamgraph.js @@ -11,7 +11,6 @@ import { getLabelPosition, recalculateOverlappingLabels, setTM, - transformData, } from "../utils/streamgraph"; import { CHART_MARGIN, @@ -37,15 +36,6 @@ class Streamgraph extends React.Component { constructor(props) { super(props); - this.stack = d3.layout - .stack() - .offset("silhouette") - .values((d) => d.values) - .x((d) => d.date) - .y((d) => d.value); - - this.nest = d3.nest().key((d) => d.key); - this.labelPositions = []; } @@ -71,16 +61,18 @@ class Streamgraph extends React.Component { const { colors } = this.props; const { width, height } = this.getDimensions(); - const parsedData = JSON.parse(this.props.data); - const transformedData = transformData(parsedData); - const streams = this.getStreams(transformedData); - const xScale = d3.time.scale().range([0, width]); const yScale = d3.scale.linear().range([height, 0]); const colorScale = d3.scale.ordinal().range(colors); - xScale.domain(d3.extent(transformedData, (d) => d.date)); - yScale.domain([0, d3.max(transformedData, (d) => d.y0 + d.y)]); + const streams = this.props.streams; + const streamEntries = streams.reduce( + (l, stream) => [...l, ...stream.values], + [] + ); + + xScale.domain(d3.extent(streamEntries, (d) => d.date)); + yScale.domain([0, d3.max(streamEntries, (d) => d.y0 + d.y)]); const area = d3.svg .area() @@ -94,7 +86,7 @@ class Streamgraph extends React.Component { this.renderBackground(container, width, height); this.renderStreams(container, streams, area, colorScale); - this.renderAxes(container, parsedData, xScale, yScale, width, height); + this.renderAxes(container, streams, xScale, yScale, width, height); this.renderLabels(container, xScale, yScale, width); this.renderTooltip(container, xScale); this.renderLineHelper(container); @@ -159,19 +151,19 @@ class Streamgraph extends React.Component { * Renders the graph axes using d3. * * @param {Object} container the d3 representation of #streamgraph-chart - * @param {Object} parsedData the parsed JSON containing the streamgraph data + * @param {Object} streams the stream array * @param {Function} xScale x coordinate scaling function * @param {Function} yScale y coordinate scaling function * @param {number} width graph width * @param {number} height graph height */ - renderAxes(container, parsedData, xScale, yScale, width, height) { + renderAxes(container, streams, xScale, yScale, width, height) { const xAxis = d3.svg .axis() .scale(xScale) .orient("bottom") .tickFormat(d3.time.format("%Y")) - .ticks(d3.time.year, Math.ceil(parsedData.x.length / MAX_TICKS_X)); + .ticks(d3.time.year, Math.ceil(streams.length / MAX_TICKS_X)); const yAxis = d3.svg .axis() @@ -509,28 +501,10 @@ class Streamgraph extends React.Component { return { width, height }; } - - getStreams(transformedData) { - const nestedEntries = this.nest.entries(transformedData); - const streams = this.stack(nestedEntries); - - streams.forEach((stream) => { - const firstTransformedEntry = transformedData.find( - (t) => t.key === stream.key - ); - if (!firstTransformedEntry) { - stream.docIds = []; - return; - } - stream.docIds = firstTransformedEntry.docIds; - }); - - return streams; - } } const mapStateToProps = (state) => ({ - data: state.streamgraph.data, + streams: state.streamgraph.streams, colors: state.streamgraph.colors, width: state.chart.streamWidth, height: state.chart.streamHeight, diff --git a/vis/js/datamanagers/DataManager.js b/vis/js/datamanagers/DataManager.js new file mode 100644 index 000000000..8dfa5e878 --- /dev/null +++ b/vis/js/datamanagers/DataManager.js @@ -0,0 +1,398 @@ +import $ from "jquery"; +import d3 from "d3"; + +import { + getDiameterScale, + getInitialCoordsScale, + getCoordsScale, + getRadiusScale, +} from "../utils/scale"; +import { + getAuthorsList, + getInternalMetric, + getListLink, + getOpenAccessLink, + getOutlink, + getVisibleMetric, + isOpenAccess, + parseCoordinate, +} from "../utils/data"; +import { transformData } from "../utils/streamgraph"; + +import DEFAULT_SCHEME from "../dataschemes/defaultScheme"; +import PaperSanitizer from "../utils/PaperSanitizer"; + +const GOLDEN_RATIO = 2.6; + +class DataManager { + config = {}; + paperProps = []; + // outputs + context = {}; + papers = []; + scalingFactors = {}; + streams = []; + areas = []; + + constructor(config, scheme = DEFAULT_SCHEME) { + this.config = config; + this.paperProps = scheme; + } + + parseData(backendData, chartSize) { + // initialize this.context + this.__parseContext(backendData); + // initialize this.papers + this.__parsePapers(backendData); + // initialize this.scalingFactors + this.__computeScalingFactors(this.papers.length); + + if (!this.config.is_streamgraph) { + // scale this.papers based on the chart size + this.__scalePapers(chartSize); + // initialize this.areas + this.__parseAreas(backendData); + // scale this.areas based on the chart size + this.__scaleAreas(chartSize); + } else { + // initialize this.streams + this.__parseStreams(backendData); + } + } + + __parseContext(backendData) { + this.context = {}; + if (typeof backendData.context === "object") { + this.context = backendData.context; + } + if (typeof this.context.params === "string") { + this.context.params = JSON.parse(this.context.params); + } + } + + __parsePapers(backendData) { + this.papers = this.__getPapersArray(backendData); + + this.__sanitizePapers(); + + this.__processPapers(); + } + + // migrated from legacy code + __getPapersArray(backendData) { + if (this.config.show_context) { + if (typeof backendData.data === "string") { + return JSON.parse(backendData.data); + } + return backendData.data; + } + if (typeof backendData.data === "object") { + return backendData.data; + } + + return backendData; + } + + __sanitizePapers() { + const paperSanitizer = new PaperSanitizer(this.config); + + paperSanitizer.checkRequiredProps(this.papers, this.paperProps); + this.papers = paperSanitizer.sanitizeProps(this.papers, this.paperProps); + paperSanitizer.checkUniqueProps(this.papers, this.paperProps); + } + + __processPapers() { + const blockedCoords = {}; + this.papers.forEach((paper) => { + paper.safe_id = this.__getSafeId(paper); + this.__escapeStrings(paper); + this.__parseAuthors(paper); + this.__parseCoordinates(paper); + while (blockedCoords[`${paper.x}:${paper.y}`]) { + this.__adjustCoordinates(paper); + } + blockedCoords[`${paper.x}:${paper.y}`] = true; + this.__parseAccess(paper); + this.__parseLink(paper); + this.__parseComments(paper); + this.__countMetrics(paper); + this.__parseTags(paper); + this.__parseKeywords(paper); + this.__parseClassification(paper); + }); + } + + // migrated from legacy code + __getSafeId(paper) { + const id = paper.id.toString(); + + return id.replace(/[^a-zA-Z0-9]/g, (s) => { + const c = s.charCodeAt(0); + if (c === 32) { + return "-"; + } + return "__" + ("000" + c.toString(16)).slice(-4); + }); + } + + __parseAuthors(paper) { + paper.authors_list = getAuthorsList( + paper.authors, + this.config.convert_author_names + ); + + paper.authors_string = paper.authors_list.join(", "); + } + + // migrated from legacy code + __escapeStrings(paper) { + const protectedProps = new Set( + this.paperProps.filter((p) => p.protected).map((p) => p.name) + ); + + for (const field in paper) { + if (typeof paper[field] === "string") { + paper[field] = $("