UNPKG

dimple-js

Version:

Dimple is an object-oriented API allowing you to create flexible axis-based charts using [d3.js](http://d3js.org "d3.js").

414 lines (398 loc) 22.8 kB
// Copyright: 2015 AlignAlytics // License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt" // Source: /src/objects/chart/methods/_getData.js // Create a dataset containing positioning information for every series this._getData = function (data, cats, agg, order, stacked, x, y, z, p, c) { // The data for this series var returnData = [], // Handle multiple category values by iterating the fields in the row and concatenate the values // This is repeated for each axis using a small anon function getField = function (axis, row) { var returnField = []; if (axis !== null) { if (axis._hasTimeField()) { returnField.push(axis._parseDate(row[axis.timeField])); } else if (axis._hasCategories()) { axis.categoryFields.forEach(function (cat) { returnField.push(row[cat]); }, this); } } return returnField; }, // Catch a non-numeric value and re-calc as count useCount = { x: false, y: false, z: false, p: false, c: false }, // If the elements are grouped a unique list of secondary category values will be required secondaryElements = { x: [], y: [] }, // Get the x and y totals for percentages. This cannot be done in the loop above as we need the data aggregated before we get an abs total. // otherwise it will wrongly account for negatives and positives rolled together. totals = { x: [], y: [], z: [], p: [] }, colorBounds = { min: null, max: null }, tot, running = { x: [], y: [], z: [], p: [] }, addedCats = [], catTotals = {}, grandTotals = { x: 0, y: 0, z: 0, p: 0 }, key, storyCat = "", orderedStoryboardArray = [], seriesCat = [], orderedSeriesArray = [], xCat = "", xSortArray = [], yCat = "", ySortArray = [], pCat = "", pSortArray = [], rules = [], sortedData = data, groupRules = []; if (this.storyboard && this.storyboard.categoryFields.length > 0) { storyCat = this.storyboard.categoryFields[0]; orderedStoryboardArray = dimple._getOrderedList(sortedData, storyCat, this.storyboard._orderRules); } // Deal with mekkos if (x && x._hasCategories() && x._hasMeasure()) { xCat = x.categoryFields[0]; xSortArray = dimple._getOrderedList(sortedData, xCat, x._orderRules.concat([{ ordering : x.measure, desc : true }])); } if (y && y._hasCategories() && y._hasMeasure()) { yCat = y.categoryFields[0]; ySortArray = dimple._getOrderedList(sortedData, yCat, y._orderRules.concat([{ ordering : y.measure, desc : true }])); } if (p && p._hasCategories() && p._hasMeasure()) { pCat = p.categoryFields[0]; pSortArray = dimple._getOrderedList(sortedData, pCat, p._orderRules.concat([{ ordering : p.measure, desc : true }])); } if (sortedData.length > 0 && cats && cats.length > 0) { // Concat is used here to break the reference to the parent array, if we don't do this, in a storyboarded chart, // the series rules to grow and grow until the system grinds to a halt trying to deal with them all. rules = [].concat(order); seriesCat = []; cats.forEach(function (cat) { if (sortedData[0][cat] !== undefined) { seriesCat.push(cat); } }, this); if (p && p._hasMeasure()) { rules.push({ ordering : p.measure, desc : true }); } else if (c && c._hasMeasure()) { rules.push({ ordering : c.measure, desc : true }); } else if (z && z._hasMeasure()) { rules.push({ ordering : z.measure, desc : true }); } else if (x && x._hasMeasure()) { rules.push({ ordering : x.measure, desc : true }); } else if (y && y._hasMeasure()) { rules.push({ ordering : y.measure, desc : true }); } orderedSeriesArray = dimple._getOrderedList(sortedData, seriesCat, rules); } sortedData.sort(function (a, b) { var returnValue = 0, categories, comp, p, q, aMatch, bMatch; if (storyCat !== "") { returnValue = orderedStoryboardArray.indexOf(a[storyCat]) - orderedStoryboardArray.indexOf(b[storyCat]); } if (xCat !== "" && returnValue === 0) { returnValue = xSortArray.indexOf(a[xCat]) - xSortArray.indexOf(b[xCat]); } if (yCat !== "" && returnValue === 0) { returnValue = ySortArray.indexOf(a[yCat]) - ySortArray.indexOf(b[yCat]); } if (pCat !== "" && returnValue === 0) { returnValue = pSortArray.indexOf(a[pCat]) - ySortArray.indexOf(b[pCat]); } if (seriesCat && seriesCat.length > 0 && returnValue === 0) { categories = [].concat(seriesCat); returnValue = 0; for (p = 0; p < orderedSeriesArray.length; p += 1) { comp = [].concat(orderedSeriesArray[p]); aMatch = true; bMatch = true; for (q = 0; q < categories.length; q += 1) { aMatch = aMatch && (a[categories[q]] === comp[q]); bMatch = bMatch && (b[categories[q]] === comp[q]); } if (aMatch && bMatch) { returnValue = 0; break; } else if (aMatch) { returnValue = -1; break; } else if (bMatch) { returnValue = 1; break; } } } return returnValue; }); // Iterate every row in the data to calculate the return values sortedData.forEach(function (d) { // Reset the index var foundIndex = -1, xField = getField(x, d), yField = getField(y, d), zField = getField(z, d), pField = getField(p, d), // Get the aggregate field using the other fields if necessary aggField = [], key, k, i, newRow, updateData; if (!cats || cats.length === 0) { aggField = ["All"]; } else { // Iterate the category fields for (i = 0; i < cats.length; i += 1) { // Either add the value of the field or the name itself. This allows users to add custom values, for example // Setting a particular color for a set of values can be done by using a non-existent final value and then coloring // by it if (d[cats[i]] === undefined) { aggField.push(cats[i]); } else { aggField.push(d[cats[i]]); } } } // Add a key key = aggField.join("/") + "_" + xField.join("/") + "_" + yField.join("/") + "_" + pField.join("/") + "_" + zField.join("/"); // See if this field has already been added. for (k = 0; k < returnData.length; k += 1) { if (returnData[k].key === key) { foundIndex = k; break; } } // If the field was not added, do so here if (foundIndex === -1) { newRow = { key: key, aggField: aggField, xField: xField, xValue: null, xCount: 0, yField: yField, yValue: null, yCount: 0, pField: pField, pValue: null, pCount: 0, zField: zField, zValue: null, zCount: 0, cValue: 0, cCount: 0, x: 0, y: 0, xOffset: 0, yOffset: 0, width: 0, height: 0, cx: 0, cy: 0, xBound: 0, yBound: 0, xValueList: [], yValueList: [], zValueList: [], pValueList: [], cValueList: [], fill: {}, stroke: {} }; returnData.push(newRow); foundIndex = returnData.length - 1; } // Update the return data for the passed axis updateData = function (axis, storyboard) { var passStoryCheck = true, lhs = { value: 0, count: 1 }, rhs = { value: 0, count: 1 }, selectStoryValue, compare = "", retRow; if (storyboard !== null) { selectStoryValue = storyboard.getFrameValue(); storyboard.categoryFields.forEach(function (cat, m) { if (m > 0) { compare += "/"; } compare += d[cat]; passStoryCheck = (compare === selectStoryValue); }, this); } if (axis !== null && axis !== undefined) { if (passStoryCheck) { retRow = returnData[foundIndex]; if (axis._hasMeasure() && d[axis.measure] !== null && d[axis.measure] !== undefined) { // Keep a distinct list of values to calculate a distinct count in the case of a non-numeric value being found if (retRow[axis.position + "ValueList"].indexOf(d[axis.measure]) === -1) { retRow[axis.position + "ValueList"].push(d[axis.measure]); } // The code above is outside this check for non-numeric values because we might encounter one far down the list, and // want to have a record of all values so far. if (isNaN(parseFloat(d[axis.measure]))) { useCount[axis.position] = true; } // Set the value using the aggregate function method lhs.value = retRow[axis.position + "Value"]; lhs.count = retRow[axis.position + "Count"]; rhs.value = d[axis.measure]; retRow[axis.position + "Value"] = agg(lhs, rhs); retRow[axis.position + "Count"] += 1; } } } }; // Update all the axes updateData(x, this.storyboard); updateData(y, this.storyboard); updateData(z, this.storyboard); updateData(p, this.storyboard); updateData(c, this.storyboard); }, this); // Get secondary elements if necessary if (x && x._hasCategories() && x.categoryFields.length > 1 && secondaryElements.x !== undefined) { groupRules = []; if (y._hasMeasure()) { groupRules.push({ ordering : y.measure, desc : true }); } secondaryElements.x = dimple._getOrderedList(sortedData, x.categoryFields[1], x._groupOrderRules.concat(groupRules)); } if (y && y._hasCategories() && y.categoryFields.length > 1 && secondaryElements.y !== undefined) { groupRules = []; if (x._hasMeasure()) { groupRules.push({ ordering : x.measure, desc : true }); } secondaryElements.y = dimple._getOrderedList(sortedData, y.categoryFields[1], y._groupOrderRules.concat(groupRules)); secondaryElements.y.reverse(); } returnData.forEach(function (ret) { if (x !== null) { if (useCount.x === true) { ret.xValue = ret.xValueList.length; } tot = (totals.x[ret.xField.join("/")] || 0) + (y._hasMeasure() ? Math.abs(ret.yValue) : 0); totals.x[ret.xField.join("/")] = tot; } if (y !== null) { if (useCount.y === true) { ret.yValue = ret.yValueList.length; } tot = (totals.y[ret.yField.join("/")] || 0) + (x._hasMeasure() ? Math.abs(ret.xValue) : 0); totals.y[ret.yField.join("/")] = tot; } if (p !== null) { if (useCount.p === true) { ret.pValue = ret.pValueList.length; } tot = (totals.p[ret.pField.join("/")] || 0) + (p._hasMeasure() ? Math.abs(ret.pValue) : 0); totals.p[ret.pField.join("/")] = tot; } if (z !== null) { if (useCount.z === true) { ret.zValue = ret.zValueList.length; } tot = (totals.z[ret.zField.join("/")] || 0) + (z._hasMeasure() ? Math.abs(ret.zValue) : 0); totals.z[ret.zField.join("/")] = tot; } if (c !== null) { if (colorBounds.min === null || ret.cValue < colorBounds.min) { colorBounds.min = ret.cValue; } if (colorBounds.max === null || ret.cValue > colorBounds.max) { colorBounds.max = ret.cValue; } } }, this); // Before calculating the positions we need to sort elements // Set all the dimension properties of the data for (key in totals.x) { if (totals.x.hasOwnProperty(key)) { grandTotals.x += totals.x[key]; } } for (key in totals.y) { if (totals.y.hasOwnProperty(key)) { grandTotals.y += totals.y[key]; } } for (key in totals.p) { if (totals.p.hasOwnProperty(key)) { grandTotals.p += totals.p[key]; } } for (key in totals.z) { if (totals.z.hasOwnProperty(key)) { grandTotals.z += totals.z[key]; } } returnData.forEach(function (ret) { var baseColor, targetColor, scale, colorVal, floatingPortion, getAxisData = function (axis, opp, size) { var totalField, value, selectValue, pos, cumValue; if (axis !== null && axis !== undefined) { pos = axis.position; if (!axis._hasCategories()) { value = (axis.showPercent ? ret[pos + "Value"] / totals[opp][ret[opp + "Field"].join("/")] : ret[pos + "Value"]); totalField = ret[opp + "Field"].join("/") + (ret[pos + "Value"] >= 0); cumValue = running[pos][totalField] = ((running[pos][totalField] === null || running[pos][totalField] === undefined || pos === "z" || pos === "p") ? 0 : running[pos][totalField]) + value; selectValue = ret[pos + "Bound"] = ret["c" + pos] = (((pos === "x" || pos === "y") && stacked) ? cumValue : value); ret[size] = value; ret[pos] = selectValue - (((pos === "x" && value >= 0) || (pos === "y" && value <= 0)) ? value : 0); } else { if (axis._hasMeasure()) { totalField = ret[axis.position + "Field"].join("/"); value = (axis.showPercent ? totals[axis.position][totalField] / grandTotals[axis.position] : totals[axis.position][totalField]); if (addedCats.indexOf(totalField) === -1) { catTotals[totalField] = value + (addedCats.length > 0 ? catTotals[addedCats[addedCats.length - 1]] : 0); addedCats.push(totalField); } selectValue = ret[pos + "Bound"] = ret["c" + pos] = (((pos === "x" || pos === "y") && stacked) ? catTotals[totalField] : value); ret[size] = value; ret[pos] = selectValue - (((pos === "x" && value >= 0) || (pos === "y" && value <= 0)) ? value : 0); } else { ret[pos] = ret["c" + pos] = ret[pos + "Field"][0]; ret[size] = 1; if (secondaryElements[pos] !== undefined && secondaryElements[pos] !== null && secondaryElements[pos].length >= 2) { ret[pos + "Offset"] = secondaryElements[pos].indexOf(ret[pos + "Field"][1]); ret[size] = 1 / secondaryElements[pos].length; } } } } }; getAxisData(x, "y", "width"); getAxisData(y, "x", "height"); getAxisData(z, "z", "r"); getAxisData(p, "p", "angle"); // If there is a color axis if (c !== null && colorBounds.min !== null && colorBounds.max !== null) { // Handle matching min and max if (colorBounds.min === colorBounds.max) { colorBounds.min -= 0.5; colorBounds.max += 0.5; } // Limit the bounds of the color value to be within the range. Users may override the axis bounds and this // allows a 2 color scale rather than blending if the min and max are set to 0 and 0.01 for example negative values // and zero value would be 1 color and positive another. colorBounds.min = (c.overrideMin || colorBounds.min); colorBounds.max = (c.overrideMax || colorBounds.max); ret.cValue = (ret.cValue > colorBounds.max ? colorBounds.max : (ret.cValue < colorBounds.min ? colorBounds.min : ret.cValue)); // Calculate the factors for the calculations scale = d3.scale.linear().range([0, (c.colors === null || c.colors.length === 1 ? 1 : c.colors.length - 1)]).domain([colorBounds.min, colorBounds.max]); colorVal = scale(ret.cValue); floatingPortion = colorVal - Math.floor(colorVal); if (ret.cValue === colorBounds.max) { floatingPortion = 1; } // If there is a single color defined if (c.colors && c.colors.length === 1) { baseColor = d3.rgb(c.colors[0]); targetColor = d3.rgb(this.getColor(ret.aggField.slice(-1)[0]).fill); } else if (c.colors && c.colors.length > 1) { baseColor = d3.rgb(c.colors[Math.floor(colorVal)]); targetColor = d3.rgb(c.colors[Math.ceil(colorVal)]); } else { baseColor = d3.rgb("white"); targetColor = d3.rgb(this.getColor(ret.aggField.slice(-1)[0]).fill); } // Calculate the correct grade of color baseColor.r = Math.floor(baseColor.r + (targetColor.r - baseColor.r) * floatingPortion); baseColor.g = Math.floor(baseColor.g + (targetColor.g - baseColor.g) * floatingPortion); baseColor.b = Math.floor(baseColor.b + (targetColor.b - baseColor.b) * floatingPortion); // Set the colors on the row ret.fill = baseColor.toString(); ret.stroke = baseColor.darker(0.5).toString(); } }, this); return returnData; };