dce-charts
Version:
Harvard DCE's React Charting Wrapper
595 lines (571 loc) • 20.1 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
var reactChartjs2 = require('react-chartjs-2');
var chart_js = require('chart.js');
var clone = require('fast-clone');
var Papa = require('papaparse');
var reactFontawesome = require('@fortawesome/react-fontawesome');
var freeSolidSvgIcons = require('@fortawesome/free-solid-svg-icons');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var clone__default = /*#__PURE__*/_interopDefaultLegacy(clone);
var Papa__default = /*#__PURE__*/_interopDefaultLegacy(Papa);
/**
* Get a css color based on a color object and opacity
* @author Jackson Parsells
* @param color the color object
* @param opacity opacity of the color
* @returns the css color as a string
*/
const getRGBA = (color, opacity) => {
return `rgba(${color.r}, ${color.g}, ${color.b}, ${opacity})`;
};
/**
* Blue preset color
* @author Gabe Abrams
*/
const BlueColor = {
r: 54,
g: 162,
b: 235,
};
/**
* Green preset color
* @author Gabe Abrams
*/
const GreenColor = {
r: 75,
g: 192,
b: 192,
};
/**
* Orange preset color
* @author Gabe Abrams
*/
const OrangeColor = {
r: 255,
g: 159,
b: 64,
};
/**
* Purple preset color
* @author Gabe Abrams
*/
const PurpleColor = {
r: 153,
g: 102,
b: 255,
};
/**
* Red preset color
* @author Gabe Abrams
*/
const RedColor = {
r: 255,
g: 99,
b: 132,
};
/**
* Yellow preset color
* @author Gabe Abrams
*/
const YellowColor = {
r: 255,
g: 206,
b: 86,
};
// Import colors
/**
* Preset available colors
* @author Gabe Abrams
*/
const presetColors = {
red: RedColor,
blue: BlueColor,
yellow: YellowColor,
green: GreenColor,
purple: PurpleColor,
orange: OrangeColor,
};
/**
* Returns true if the two colors are the same
* @author Gabe Abrams
* @param a first color
* @param b second color
* @returns true if colors are the same
*/
const colorsAreSame = (a, b) => {
return (a.r === b.r
&& a.g === b.g
&& a.b === b.b);
};
// Import libs
// List of all colors in the order they will be distributed
const allColors = [
presetColors.red,
presetColors.blue,
presetColors.yellow,
presetColors.green,
presetColors.orange,
presetColors.purple,
];
/**
* Add colors to elements that don't have colors
* @author Gabe Abrams
* @param items list of items that may or may not have a color prop
* @returns items with a color prop defined on every item
*/
const addColors = (items) => {
var _a;
// If one or zero items, this is simple: just give it a color
if (items.length === 0) {
return [];
}
if (items.length === 1) {
return [Object.assign(Object.assign({}, items), { color: ((_a = items[0].color) !== null && _a !== void 0 ? _a : allColors[0]) })];
}
// first clone items
const itemsClone = clone__default["default"](items);
// Index of the next color
let nextColorIndex = 0;
itemsClone.forEach((item, index) => {
// If user includes a color, just keep it
if (item.color) {
return;
}
// Get the index of the previous and next colors
const prevItemColor = itemsClone[(index === 0
? itemsClone.length - 1
: index - 1)];
const nextItemColor = itemsClone[(index === itemsClone.length - 1
? 0
: index + 1)];
// Loop until we get a unique color
let color = allColors[nextColorIndex];
for (let i = 0; i < 3; i++) {
// Get the next candidate color
const candidateColor = allColors[(nextColorIndex + i) % allColors.length];
// Check if this candidate color matches the prev/next color
const candidateColorIsSame = (colorsAreSame(candidateColor, prevItemColor)
|| colorsAreSame(candidateColor, nextItemColor));
// If this is a unique color, keep it!
if (!candidateColorIsSame) {
// Save the color
color = candidateColor;
// Increment next color index
nextColorIndex += (i + 1);
break;
}
}
// Save the color to the item
itemsClone[index].color = color;
});
// return the cloned list
return itemsClone;
};
/**
* Component that's a clickable button to download the contents of a chart as
* a CSV file
* @author Jackson Parsells
* @author Gabe Abrams
*/
/*------------------------------------------------------------------------*/
/* Component */
/*------------------------------------------------------------------------*/
const DownloadButton = (props) => {
/*------------------------------------------------------------------------*/
/* Setup */
/*------------------------------------------------------------------------*/
/* -------------- Props ------------- */
// destructure props
const { title, chartData, filename, } = props;
/*------------------------------------------------------------------------*/
/* Render */
/*------------------------------------------------------------------------*/
/*----------------------------------------*/
/* Main UI */
/*----------------------------------------*/
// Generate CSV text
const csvText = Papa__default["default"].unparse(chartData);
// Download button
return (React__default["default"].createElement("a", { href: `data:text/csv;charset=utf-8,${encodeURI(csvText)}`, download: filename, className: "btn btn-sm btn-primary", style: {
borderTopLeftRadius: 0,
borderBottomRightRadius: 0,
}, title: "Download Data as CSV", "aria-label": `download the data of ${title} as a csv file` },
React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faDownload })));
};
/**
* Download box container that wraps a chart
* @author Gabe Abrams
* @author Jackson Parsells
*/
/*------------------------------------------------------------------------*/
/* Style */
/*------------------------------------------------------------------------*/
const style = `
.DownloadBox-download {
position: absolute;
top: 0;
right: 15px;
cursor: pointer;
}
.DownloadBox-no-outline {
border: 1px solid transparent;
}
.DownloadBox-outlined {
border: 1px solid #007bff;
}
`;
/* ------------- Actions ------------ */
// Types of actions
var ActionType;
(function (ActionType) {
// Start hovering
ActionType["StartHovering"] = "start-hovering";
// Stop hovering
ActionType["StopHovering"] = "stop-hovering";
})(ActionType || (ActionType = {}));
/**
* Reducer that executes actions
* @author Gabe Abrams
* @param state current state
* @param action action to execute
*/
const reducer = (state, action) => {
switch (action.type) {
case ActionType.StartHovering: {
return Object.assign(Object.assign({}, state), { hovering: true });
}
case ActionType.StopHovering: {
return Object.assign(Object.assign({}, state), { hovering: false });
}
default: {
return state;
}
}
};
/*------------------------------------------------------------------------*/
/* Component */
/*------------------------------------------------------------------------*/
const DownloadBox = (props) => {
/*------------------------------------------------------------------------*/
/* Setup */
/*------------------------------------------------------------------------*/
/* -------------- Props ------------- */
// Destructure all props
const { title, chartData, children, } = props;
/* -------------- State ------------- */
// Initial state
const initialState = {
hovering: false,
};
// Initialize state
const [state, dispatch] = React.useReducer(reducer, initialState);
// Destructure common state
const { hovering, } = state;
/*------------------------------------------------------------------------*/
/* Render */
/*------------------------------------------------------------------------*/
/*----------------------------------------*/
/* Main UI */
/*----------------------------------------*/
return (React__default["default"].createElement("div", {
// use similar logic to below to add for focus and hovering
className: hovering ? 'DownloadBox-outlined' : 'DownloadBox-no-outline', onMouseEnter: () => {
dispatch({ type: ActionType.StartHovering });
}, onFocus: () => {
dispatch({ type: ActionType.StartHovering });
}, onMouseLeave: () => {
dispatch({ type: ActionType.StopHovering });
}, onBlur: () => {
dispatch({ type: ActionType.StopHovering });
} },
React__default["default"].createElement("style", null, style),
children,
React__default["default"].createElement("div", { className: `DownloadBox-download ${hovering ? '' : 'sr-only'}` },
React__default["default"].createElement(DownloadButton, { title: title, chartData: chartData, filename: `${title}.csv` }))));
};
/**
* DoughnutChart, wrapper around the ChartJS Doughnut chart for DCE widgets
* @author Jackson Parsells
*/
// chart JS registry
chart_js.Chart.register(chart_js.ArcElement, chart_js.Tooltip, chart_js.Legend);
/*------------------------------------------------------------------------*/
/* Component */
/*------------------------------------------------------------------------*/
const DoughnutChart = (props) => {
/*------------------------------------------------------------------------*/
/* Setup */
/*------------------------------------------------------------------------*/
/* -------------- Props ------------- */
const { title, showTitle = true, segments, showLegend = true, } = props;
// Add colors to props
const segmentsWithColors = addColors(segments);
// Create label list
const labels = segmentsWithColors.map((segment) => {
return segment.label;
});
// Create dataset
const dataset = {
label: '',
data: segmentsWithColors.map((segment) => {
return segment.value;
}),
backgroundColor: segmentsWithColors.map((segment) => {
return getRGBA(segment.color, 0.2);
}),
borderColor: segmentsWithColors.map((segment) => {
return getRGBA(segment.color, 1);
}),
borderWidth: 1,
};
// data for CSV
const dataForCSV = segments.map((segment) => {
return {
Name: segment.label,
Value: segment.value,
};
});
// chart data
const chartData = {
labels,
datasets: [dataset],
};
// chart options
const options = {
responsive: true,
plugins: {
legend: {
display: showLegend,
position: 'top',
},
title: {
display: showTitle,
text: title,
},
},
};
/*----------------------------------------*/
/* Main UI */
/*----------------------------------------*/
return (React__default["default"].createElement(DownloadBox, { chartData: dataForCSV, title: title },
React__default["default"].createElement(reactChartjs2.Doughnut, { data: chartData, options: options })));
};
/**
* LineChart, wrapper around the ChartJS Line chart for DCE widgets
* @author Jackson Parsells
*/
// chart JS registry
chart_js.Chart.register(chart_js.CategoryScale, chart_js.LinearScale, chart_js.PointElement, chart_js.LineElement, chart_js.Title, chart_js.Tooltip, chart_js.Legend);
/*------------------------------------------------------------------------*/
/* Component */
/*------------------------------------------------------------------------*/
const LineChart = (props) => {
/*------------------------------------------------------------------------*/
/* Setup */
/*------------------------------------------------------------------------*/
/* -------------- Props ------------- */
const { title, showTitle = true, points, } = props;
// Add colors to props
const pointsWithColors = addColors(points);
// Create label list
const labels = pointsWithColors.map((point) => {
return point.label;
});
// Create dataset
const dataset = {
label: '',
data: pointsWithColors.map((point) => {
return point.value;
}),
backgroundColor: pointsWithColors.map((point) => {
return getRGBA(point.color, 0.2);
}),
borderColor: pointsWithColors.map((point) => {
return getRGBA(point.color, 1);
}),
borderWidth: 1,
};
// data for CSV
const dataForCSV = points.map((point) => {
return {
Name: point.label,
Value: point.value,
};
});
// chart data
const chartData = {
labels,
datasets: [dataset],
};
// chart options
const options = {
responsive: true,
plugins: {
legend: {
display: false,
position: 'top',
},
title: {
text: title,
display: showTitle,
},
},
};
/*----------------------------------------*/
/* Main UI */
/*----------------------------------------*/
return (React__default["default"].createElement(DownloadBox, { chartData: dataForCSV, title: title },
React__default["default"].createElement(reactChartjs2.Line, { data: chartData, options: options })));
};
/**
* HorizontalBarChart, wrapper around the ChartJS Horizontal Bar chart for DCE
* widgets
* @author Jackson Parsells
*/
// chart JS registry
chart_js.Chart.register(chart_js.CategoryScale, chart_js.LinearScale, chart_js.BarElement, chart_js.Title, chart_js.Tooltip, chart_js.Legend);
/*------------------------------------------------------------------------*/
/* Component */
/*------------------------------------------------------------------------*/
const HorizontalBarChart = (props) => {
/*------------------------------------------------------------------------*/
/* Setup */
/*------------------------------------------------------------------------*/
/* -------------- Props ------------- */
// destructure props
const { title, showTitle = true, bars, } = props;
// add colors to props
const barsWithColors = addColors(bars);
// create label list
const labels = barsWithColors.map((bar) => {
return bar.label;
});
// create dataset
const dataset = {
label: '',
data: barsWithColors.map((bar) => {
return bar.value;
}),
backgroundColor: barsWithColors.map((bar) => {
return getRGBA(bar.color, 0.2);
}),
borderColor: barsWithColors.map((bar) => {
return getRGBA(bar.color, 1);
}),
borderWidth: 1,
};
// data for CSV
const dataForCSV = bars.map((bar) => {
return {
Name: bar.label,
Value: bar.value,
};
});
// chart data
const chartData = {
labels,
datasets: [dataset],
};
// chart options to have the bars extend horizontally
const options = {
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
display: false,
position: 'top',
},
title: {
display: showTitle,
text: title,
},
},
};
/*----------------------------------------*/
/* Main UI */
/*----------------------------------------*/
return (React__default["default"].createElement(DownloadBox, { chartData: dataForCSV, title: title },
React__default["default"].createElement(reactChartjs2.Bar, { data: chartData, options: options })));
};
/**
* VerticalBarChart, wrapper around the ChartJS Vertical Bar chart for DCE
* widgets
* @author Jackson Parsells
*/
// chart JS registry
chart_js.Chart.register(chart_js.CategoryScale, chart_js.LinearScale, chart_js.BarElement, chart_js.Title, chart_js.Tooltip, chart_js.Legend);
/*------------------------------------------------------------------------*/
/* Component */
/*------------------------------------------------------------------------*/
const VerticalBarChart = (props) => {
/*------------------------------------------------------------------------*/
/* Setup */
/*------------------------------------------------------------------------*/
/* -------------- Props ------------- */
const { title, showTitle = true, bars, } = props;
// add colors to props
const barsWithColors = addColors(bars);
// create label list
const labels = barsWithColors.map((bar) => {
return bar.label;
});
// create dataset
const dataset = {
label: '',
data: barsWithColors.map((bar) => {
return bar.value;
}),
backgroundColor: barsWithColors.map((bar) => {
return getRGBA(bar.color, 0.2);
}),
borderColor: barsWithColors.map((bar) => {
return getRGBA(bar.color, 1);
}),
borderWidth: 1,
};
// data for CSV
const dataForCSV = bars.map((bar) => {
return {
Name: bar.label,
Value: bar.value,
};
});
// chart data
const chartData = {
labels,
datasets: [dataset],
};
// chart options to make the bar vertical
const options = {
responsive: true,
plugins: {
legend: {
display: false,
position: 'top',
},
title: {
display: showTitle,
text: title,
},
},
};
/*----------------------------------------*/
/* Main UI */
/*----------------------------------------*/
return (React__default["default"].createElement(DownloadBox, { chartData: dataForCSV, title: title },
React__default["default"].createElement(reactChartjs2.Bar, { data: chartData, options: options })));
};
exports.BlueColor = BlueColor;
exports.DoughnutChart = DoughnutChart;
exports.GreenColor = GreenColor;
exports.HorizontalBarChart = HorizontalBarChart;
exports.LineChart = LineChart;
exports.OrangeColor = OrangeColor;
exports.PurpleColor = PurpleColor;
exports.RedColor = RedColor;
exports.VerticalBarChart = VerticalBarChart;
exports.YellowColor = YellowColor;
//# sourceMappingURL=index.js.map