diff --git a/package.json b/package.json index b77e4b51f2b910cd329cd298141aca11a0044ce4..998af5cb6df043f09510e61f81481651ac74e041 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,12 @@ "version": "1.0.0", "private": true, "dependencies": { + "@popperjs/core": "^2.11.4", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^13.2.1", "@types/jest": "^27.0.1", + "@types/lodash": "^4.14.118", "@types/node": "^16.7.13", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", @@ -14,12 +16,19 @@ "@types/sortablejs": "^1.10.7", "axios": "^0.25.0", "bootstrap": "^5.1.3", + "chart.js": "^3.4.0", + "chartjs-plugin-datalabels": "^2.0.0", + "dom-to-image": "^2.6.0", + "file-saver": "^2.0.5", + "lodash": "^4.17.11", "moment": "^2.29.1", "react": "^17.0.2", + "react-chartjs-2": "^3.0.3", "react-confirm-alert": "^2.7.0", "react-dom": "^17.0.2", "react-loadable": "^5.5.0", "react-localization": "^1.0.17", + "react-modal": "^3.14.4", "react-router-dom": "5.2.0", "react-scripts": "5.0.0", "react-toastify": "^8.1.0", diff --git a/src/Router.js b/src/Router.js index 5c8c7f1545aa9266ab6acebca312d3f9908ff108..d3f49600352490827eeee5f9397d98dc102f7b71 100644 --- a/src/Router.js +++ b/src/Router.js @@ -15,7 +15,8 @@ import { ReviewApplication, InspectionSummary, InspectionComplete, - ViewConsentApplications + ViewConsentApplications, + Landing, } from "./pages"; // import ReviewerApplications from "./pages/Reviewer/ReviewerApplications"; import { Manage } from "./pages/Reviewer/manage"; @@ -31,6 +32,7 @@ const Router = (props) => ( <Route exact path="/" component={Login} /> <Route path="/login" component={Login} /> <PrivateRoute path="/dashboard" component={Dashboard} /> + <PrivateRoute path="/analytics" component={Landing} /> <PrivateRoute exact path="/forms" component={ListForms} /> <PrivateRoute exact path="/forms/add" component={AddForm} /> <PrivateRoute exact path="/forms/:id/edit" component={AddForm} /> @@ -52,7 +54,7 @@ const Router = (props) => ( path="/inspector/:id/:applicationId" component={ViewApplications} /> - <PrivateRoute + <PrivateRoute exact path="/assisting-inspector/:id/:applicationId" component={ViewConsentApplications} diff --git a/src/components/charts/BarChart.js b/src/components/charts/BarChart.js new file mode 100644 index 0000000000000000000000000000000000000000..86859a908c8e8238e3a6e6d736ab4c5e716b8cfd --- /dev/null +++ b/src/components/charts/BarChart.js @@ -0,0 +1,176 @@ +import React from "react"; +import { Bar } from "react-chartjs-2"; +import NFormatterFun from "./NFormatterFun"; +import _ from "lodash"; +import barPalette from "./palette"; + +/** + * BarChart component + */ + +const options = { + scales: { + y: { + grid: { + display: true, + }, + ticks: { + font: { + family: "Lato-Regular", + size: 12, + lineHeight: 1.333, + }, + }, + }, + x: { + grid: { + display: true, + }, + ticks: { + font: { + family: "Lato-Regular", + size: 12, + lineHeight: 1.333, + }, + }, + }, + }, + responsive: true, + options: { + responsive: true, + maintainAspectRatio: true, + }, + plugins: { + legend: { + position: "bottom", + display: true, + labels: { + font: { + family: "Lato-Regular", + size: 14, + lineHeight: 1.429, + }, + }, + }, + datalabels: { + anchor: "end", + align: "end", + }, + }, +}; + +class BarChart extends React.Component { + constructor(props) { + super(props); + this.state = { + trigger: "", + chartsGData: {}, + isData: false, + }; + } + + manupulateData(chartData) { + var barTempData = { + labels: [], + datasets: [], + }; + let colors = barPalette("cb-BasePalette2", chartData.length).map(function ( + hex + ) { + return "#" + hex; + }); + chartData.forEach((d, i) => { + let barTempObj = { + label: "", + borderColor: colors[i], + backgroundColor: colors[i], + fill: false, + }; + let tempdataArr = []; + let tempdatalabel = [], + tempVal = ""; + barTempObj.label = d.headerName; + d.plots.forEach((d1, i) => { + tempVal = NFormatterFun(d1.value, d1.symbol, "Unit"); + tempVal = + typeof tempVal == "string" + ? parseFloat(tempVal.replace(/,/g, "")) + : tempVal; + tempdataArr.push(d1.value); + if (d1.name.length > 20) { + tempdatalabel.push(d1.name.match(/.{1,20}\S+\s*/g)); + } else { + tempdatalabel.push(d1.name); + } + }); + barTempObj.data = tempdataArr; + barTempData.labels = tempdatalabel; + barTempData.datasets.push(barTempObj); + }); + + return barTempData; + } + + contextMenu = (e) => { + e.preventDefault(); + + this.setState({ + isData: false, + }); + }; + + render() { + let { chartData } = this.props; + let { drillDownId } = this.props; + let data; + + /* + * Drilldown chart data + */ + let drillDownData = _.chain(this.state.chartsGData) + .get(drillDownId) + .get("data") + .value(); + + /* + * Condition to load drill down datasets + * if user enabled the drill down feature + */ + if (this.state.isData) { + if (drillDownData !== undefined) { + data = this.manupulateData(drillDownData); + } else { + data = this.manupulateData(chartData); + } + } else { + data = this.manupulateData(chartData); + } + + if (data) { + return ( + <div onContextMenu={this.contextMenu}> + <Bar + height={this.props.dimensions.height} + style={{ fill: "none" }} + data={data} + options={options} + plugins={[ + { + beforeInit: (chart, options) => { + chart.legend.afterFit = () => { + if (chart.legend.margins) { + chart.legend.options.labels.padding = 20; + } + }; + }, + }, + ]} + ></Bar> + </div> + ); + } + return <div>Loading...</div>; + } +} + +export default BarChart; diff --git a/src/components/charts/ChartType.js b/src/components/charts/ChartType.js new file mode 100644 index 0000000000000000000000000000000000000000..d7c5b875bdd7b1e9f581855c3916368eac55c33a --- /dev/null +++ b/src/components/charts/ChartType.js @@ -0,0 +1,187 @@ +import React from "react"; +import LineChart from "./LineChart"; +import BarChart from "./BarChart"; +import PieChart from "./PieChart"; +import MixedData from "./MixedData"; +import MetricVisual from "./MetricVisual"; +import _ from "lodash"; +import { ChartService } from "../../services"; +import ExportChart from "../../helpers/exportChart"; +import moment from "moment"; + +/** + * Component to genearte the required charts + * as per the response from the API + */ + +class ChartType extends React.Component { + constructor(props) { + super(props); + this.state = { + chartsGData: {}, + }; + } + + componentDidMount() { + this.callAPI(); + } + + callAPI() { + let code = _.chain(this.props).get("chartData").first().get("id").value(); + + let startRange = moment().startOf("month"); + let endRange = moment().endOf("month"); + startRange = Number(startRange); + endRange = Number(endRange); + let thisMonth = { startDate: startRange, endDate: endRange }; + + let payload = { + RequestInfo: { + authToken: "null", + }, + headers: { + tenantId: "null", + }, + aggregationRequestDto: { + dashboardId: "home", + visualizationType: "METRIC", + visualizationCode: code, + queryType: "", + filters: {}, + moduleLevel: "", + aggregationFactors: null, + requestDate: thisMonth, + }, + }; + + ChartService.getChartData(payload).then( + (response) => { + this.setState((prevState) => ({ + ...prevState, + chartsGData: { + ...prevState.chartsGData, + [code]: response.responseData, + }, + })); + }, + (error) => {} + ); + } + + render() { + let chartKey = _.chain(this.props) + .get("chartData") + .first() + .get("id") + .value(); + + let chartType = _.chain(this.props) + .get("chartData") + .first() + .get("chartType") + .toUpper() + .value(); + + let data = _.chain(this.state) + .get("chartsGData") + .get(chartKey) + .get("data") + .value(); + + let filter = _.chain(this.props) + .get("chartData") + .first() + .get("filter") + .value(); + + let deepFilter = _.chain(this.state) + .get("chartsGData") + .get(chartKey) + .get("filter") + .value(); + + let drillDownId = _.chain(this.state) + .get("chartsGData") + .get(chartKey) + .get("drillDownChartId") + .value(); + + if (filter) { + localStorage.setItem("filterKey", filter); + } else if (deepFilter) { + localStorage.setItem("filterKey", deepFilter); + } + + if (data) { + ExportChart.setAttribute(chartKey, data); + + switch (chartType) { + case "PIE": + return ( + <PieChart + chartData={data} + label={this.props.label} + unit={this.state.unit} + GFilterData={this.props.GFilterData} + dimensions={this.props.dimensions} + section={this.props.section} + pathName={this.props.pathName.pathProps} + /> + ); + case "LINE": + return ( + <LineChart + chartData={data} + label={this.props.label} + unit={this.state.unit} + GFilterData={this.props.GFilterData} + dimensions={this.props.dimensions} + section={this.props.section} + pathName={this.props.pathName.pathProps} + /> + ); + case "BAR": + return ( + <BarChart + chartData={data} + label={this.props.label} + unit={this.state.unit} + GFilterData={this.props.GFilterData} + dimensions={this.props.dimensions} + section={this.props.section} + pathName={this.props.pathName.pathProps} + drillDownId={drillDownId} + filter={filter} + /> + ); + case "LINE_BAR": + return ( + <MixedData + chartData={data} + label={this.props.label} + unit={this.state.unit} + GFilterData={this.props.GFilterData} + dimensions={this.props.dimensions} + section={this.props.section} + /> + ); + case "METRICCOLLECTION": + return ( + <MetricVisual + chartData={data} + label={this.props.label} + unit={this.state.unit} + GFilterData={this.props.GFilterData} + dimensions={this.props.dimensions} + section={this.props.section} + /> + ); + default: + return false; + } + } + return <div> Loading... </div>; + } +} + +export default ChartType; diff --git a/src/components/charts/GenericCharts.js b/src/components/charts/GenericCharts.js new file mode 100644 index 0000000000000000000000000000000000000000..6f2d361bab6f77fb450cf1aa08a26a7deb3c7a8c --- /dev/null +++ b/src/components/charts/GenericCharts.js @@ -0,0 +1,353 @@ +import React from "react"; +import ChartType from "./ChartType"; +import "file-saver"; +import domtoimage from "dom-to-image"; +import Modal from "react-modal"; +import ExportChart from "../../helpers/exportChart"; +import _ from "lodash"; +import "../../styles/chart.css"; + +/** + * GenericChart component to display the + * generated charttypes in the page layout + */ + +Modal.setAppElement("#root"); + +class GenericCharts extends React.Component { + constructor(props) { + super(props); + this.state = { + modalData: { + name: "", + data: [], + chartDataName: "", + dimensions: "", + }, + modalIsOpen: false, + imageBlob: "", + imageBase64: "", + s3URL: "", + telegramURL: "", + chartCode: "", + chartDType: "", + }; + this.getModalData = this.getModalData.bind(this); + this.openModal = this.openModal.bind(this); + this.closeModal = this.closeModal.bind(this); + this.afterOpenModal = this.afterOpenModal.bind(this); + } + + filterImage = (node) => { + return ( + node.id !== "dropdownMenuButton" && + node.id !== "zoomIn" && + node.id !== "zoomOut" && + node.id !== "zoomInBtn" && + node.id !== "zoomOutBtn" && + node.id !== "fullScreenBtn" + ); + }; + + filterImageTwo = (node) => { + return ( + node.id !== "exit" && + node.id !== "downloadAsData" && + node.id !== "downloadAsImage" && + node.id !== "dropdownMenuButtonShare" && + node.id !== "fullScreenBtn" + ); + }; + + openModal = () => { + this.setState({ + modalIsOpen: true, + }); + }; + + afterOpenModal = () => {}; + + closeModal = () => { + this.setState({ + modalIsOpen: false, + }); + }; + + getModalData = (name, description, data, cdName, dimensions) => { + // console.log(name, data, cdName); + this.setState( + { + modalData: { + ...this.state.modalData, + name: name, + description: description, + data: data, + chartDataName: cdName, + dimensions: dimensions, + }, + showModal: true, + chartCode: _.chain(data).first().get("id").value(), + chartDType: _.chain(data).first().get("chartType").toUpper().value(), + }, + () => { + this.openModal(); + } + ); + }; + + renderCharts(d, chartData, index) { + switch (d.vizType.toUpperCase()) { + case "CHART": + return ( + <div + key={index} + className={`col-sm-12 col-md-${d.dimensions.width} col-lg-${d.dimensions.width} mt-2 mb-3`} + > + <div + className="chart-wrapper h-100 chart_card_one chart-wrapper-padding-2" + id={d.name} + > + <div className="clearfix mb-3"> + <div className="float-start"> + <h5 className="pt-1 chart_title_one mb-0">{d.name}</h5> + {d.description && ( + <label className="chart_card_description_one"> + {d.description} + </label> + )} + </div> + <div className="float-end"> + <div className="d-flex"> + <div + id="fullScreenBtn" + className="material-icons custom_cursor mt-2 black-60" + onClick={() => { + this.getModalData( + d.name, + d.description, + d.charts, + chartData.name, + d.dimensions + ); + }} + > + fullscreen + </div> + <div className="dropdown"> + <div + className="material-icons custom_cursor mt-2 ms-2 black-60" + id="dropdownMenuButton" + data-bs-toggle="dropdown" + aria-expanded="false" + > + more_vert + </div> + <div + className="dropdown-menu dropdown-menu-custom p-0 m-0" + aria-labelledby="dropdownMenuButton" + > + <p + className="dropdown-item custom_cursor dropdown_text mt-3" + onClick={() => + setTimeout(() => { + domtoimage + .toBlob(document.getElementById(d.name), { + filter: this.filterImage, + }) + .then((blob) => window.saveAs(blob, d.name)); + }, 250) + } + > + Download as PNG + </p> + </div> + </div> + </div> + </div> + </div> + + {/* Modal */} + <Modal + id="modalView" + isOpen={this.state.modalIsOpen} + onAfterOpen={this.afterOpenModal} + onRequestClose={this.closeModal} + contentLabel={this.state.modalData.name} + className="custom-modal" + bodyOpenClassName="afterOpen" + overlayClassName="custom-modal-overlay" + > + <div> + <div className="clearfix"> + <div className="float-start"></div> + <div className="float-end custom-modal-font-2"> + <div className="d-flex"> + <div + id="downloadAsImage" + className="custom_cursor " + onClick={() => + domtoimage + .toBlob(document.getElementById("modalView"), { + filter: this.filterImageTwo, + }) + .then((blob) => { + this.setState( + { + imageBlob: blob, + }, + () => { + let reader = new FileReader(); + reader.readAsDataURL(this.state.imageBlob); + + reader.onload = function () { + this.image = reader.result; + }; + window.saveAs( + this.state.imageBlob, + this.state.modalData.name + ); + } + ); + }) + } + > + <span className="float-end me-3"> + Download as image + </span> + <span className="material-icons float-end me-2"> + cloud_download + </span> + </div> + + <div className="custom_cursor " id="downloadAsData"> + <span + className="float-end me-3" + onClick={() => + this.state.chartDType === "TABLE" + ? ExportChart.tableToCsv( + this.state.modalData.name + ) + : ExportChart.toCsv( + this.state.modalData.name, + this.state.chartCode + ) + } + > + Download data + </span> + </div> + <div + className="custom_cursor " + onClick={this.closeModal} + id="exit" + > + <span className="float-end">Exit</span> + <span className="material-icons float-right me-1"> + fullscreen_exit + </span> + </div> + </div> + </div> + </div> + <h2 className="chart-header-style-1"> + {this.state.modalData.name} + </h2> + {this.state.modalData.description && ( + <label className="chart-desc-style-1 pb-3"> + {this.state.modalData.description} + </label> + )} + </div> + + <div className="col-xs-12 col-sm-12 col-md-9 col-lg-8 col-xl-12 centerAlign"> + <ChartType + key={index} + chartData={this.state.modalData.data} + label={this.state.modalData.name} + section={this.state.modalData.chartDataName} + pathName={this.props} + dimensions={this.state.modalData.dimensions} + /> + </div> + </Modal> + + {/* Visualisation */} + <ChartType + key={index} + chartData={d.charts} + label={d.name} + section={chartData.name} + pathName={this.props} + dimensions={d.dimensions} + /> + </div> + </div> + ); + case "METRICCOLLECTION": + return ( + <div + key={index} + className={`col-sm-12 col-md-${d.dimensions.width} col-lg-${d.dimensions.width} mt-2 mb-3`} + > + <div + className="chart-wrapper h-100 cardChart chartWrapperPadding" + id={d.name} + > + <div className="row"> + <h5 className="pb-5 pt-2 pl-3">{d.name}</h5> + <img + className="custom_cursor mt-3 downloadBtn downloadIcon ml-3" + src="data:image/png;base64,R0lGODlhFAAUAIAAAP///wAAACH5BAEAAAAALAAAAAAUABQAAAIRhI+py+0Po5y02ouz3rz7rxUAOw==" + title="Download as PNG" + alt="download chart" + width="13" + height="13" + id="dropdownMenuButton" + data-bs-toggle="dropdown" + ></img> + <div + className="dropdown-menu dropdown-menu-custom" + aria-labelledby="dropdownMenuButton" + style={{ marginLeft: "-11.85em" }} + > + <p + className="dropdown-item custom_cursor metricTextColor" + onClick={() => + setTimeout(() => { + domtoimage + .toBlob(document.getElementById(d.name), { + filter: this.filterImage, + }) + .then((blob) => window.saveAs(blob, d.name)); + }, 250) + } + > + Download as PNG + </p> + </div> + </div> + <ChartType + key={index} + chartData={d.charts} + label={d.name} + section={chartData.name} + /> + </div> + </div> + ); + default: + return <div></div>; + } + } + render() { + let { chartData, row } = this.props; + + return ( + <div key={row} className="row"> + {chartData.vizArray.map((d, i) => this.renderCharts(d, chartData, i))} + </div> + ); + } +} + +export default GenericCharts; diff --git a/src/components/charts/LineChart.js b/src/components/charts/LineChart.js new file mode 100644 index 0000000000000000000000000000000000000000..5f272200328d35badbd2f7d2dfce667a3dfaf866 --- /dev/null +++ b/src/components/charts/LineChart.js @@ -0,0 +1,178 @@ +//Line Chart +import React from "react"; +import { Line } from "react-chartjs-2"; +import NFormatterFun from "./NFormatterFun"; +import linePalette from "./palette"; + +/** + * LineChart component + */ + +const options = { + elements: { + point: { + radius: 0, + }, + line: { + tension: 0, + }, + }, + scales: { + y: { + grid: { + display: true, + }, + ticks: { + font: { + family: "Lato-Regular", + size: 12, + lineHeight: 1.333, + }, + }, + }, + x: { + grid: { + display: true, + }, + ticks: { + font: { + family: "Lato-Regular", + size: 12, + lineHeight: 1.333, + }, + }, + }, + }, + responsive: true, + options: { + responsive: true, + maintainAspectRatio: true, + }, + plugins: { + legend: { + position: "bottom", + display: true, + labels: { + font: { + family: "Lato-Regular", + size: 14, + lineHeight: 1.429, + }, + }, + }, + datalabels: { + anchor: "end", + align: "end", + }, + }, +}; + +class LineChart extends React.Component { + constructor(props) { + super(props); + this.state = { + trigger: "", + }; + } + + /** + * Function to update the chart visualization + */ + updateLineVisuals = () => { + this.setState({ + trigger: true, + }); + this.props.pathName.history.push({ + pathName: "/dashboards", + state: { trigger: this.state.trigger }, + }); + }; + + manupulateData(chartData) { + var tempdata = { + labels: [], + datasets: [], + }; + + chartData.forEach((d, i) => { + let colors = linePalette("cb-BasePalette2", chartData.length).map( + function (hex) { + return "#" + hex; + } + ); + let tempObj = { + label: "", + borderColor: colors[i], + backgroundColor: colors[i], + fill: false, + }; + + let tempdataArr = []; + let tempdatalabel = [], + tempVal = ""; + tempObj.label = d.headerName; + d.plots.forEach((d1, i) => { + tempVal = NFormatterFun(d1.value, d1.symbol, "Unit"); + tempVal = + typeof tempVal == "string" + ? parseFloat(tempVal.replace(/,/g, "")) + : tempVal; + tempdataArr.push(d1.value); + if (d1.name.length > 20) { + tempdatalabel.push(d1.name.match(/.{1,20}\S+\s*/g)); + } else { + tempdatalabel.push(d1.name); + } + }); + tempObj.data = tempdataArr; + tempdata.labels = tempdatalabel; + tempdata.datasets.push(tempObj); + }); + return tempdata; + } + + render() { + let { chartData } = this.props; + let data = this.manupulateData(chartData); + + /* + * Function to get the chart label title + */ + const getLineLabelFilter = (elems) => { + if (localStorage.getItem("filterKey") && elems[0] !== undefined) { + let index = elems[0]._datasetIndex; + let selectedLabel = elems[0]._xScale.chart.data.datasets[index].label; + localStorage.setItem("label", selectedLabel); + this.updateLineVisuals(); + } else { + // console.log("Out!"); + } + }; + + if (data) { + return ( + <Line + height={this.props.dimensions.height} + style={{ fill: "none" }} + data={data} + options={options} + plugins={[ + { + beforeInit: (chart, options) => { + chart.legend.afterFit = () => { + if (chart.legend.margins) { + chart.legend.options.labels.padding = 20; + } + }; + }, + }, + ]} + getElementAtEvent={(elems) => getLineLabelFilter(elems)} + ></Line> + ); + } + return <div>Loading...</div>; + } +} + +export default LineChart; diff --git a/src/components/charts/MetricVisual.js b/src/components/charts/MetricVisual.js new file mode 100644 index 0000000000000000000000000000000000000000..404a19f49fa8da04eb92e014bbf3a9b9d36e06f5 --- /dev/null +++ b/src/components/charts/MetricVisual.js @@ -0,0 +1,37 @@ +import React from "react"; + +/** + * Metric visual component + */ + +class MetricVisual extends React.Component { + getData(chartData) { + return chartData; + } + + render() { + let { chartData } = this.props; + let _data = this.getData(chartData); + + if (_data) { + return ( + <div className=""> + {_data.map((value, index) => ( + <div className="customLineHeight" key={index}> + <p className="metricTextColor">{value.headerName}</p> + {!value.isDecimal ? ( + <p className="largeNum">{Math.round(value.headerValue)}</p> + ) : ( + <p className="largeNum">{value.headerValue}</p> + )} + <p> </p> + </div> + ))} + </div> + ); + } + return <div>Loading...</div>; + } +} + +export default MetricVisual; diff --git a/src/components/charts/MixedData.js b/src/components/charts/MixedData.js new file mode 100644 index 0000000000000000000000000000000000000000..4d3478cd95373005a888600e8eada5712602fd3c --- /dev/null +++ b/src/components/charts/MixedData.js @@ -0,0 +1,148 @@ +import React from "react"; +import { Bar } from "react-chartjs-2"; + +/** + * MixedData component + */ + +const options = { + elements: { + point: { + radius: 0, + }, + line: { + tension: 0, + }, + }, + scales: { + xAxes: [ + { + gridLines: { + color: "rgba(0, 0, 0, 0)", + display: false, + }, + }, + ], + yAxes: [ + { + gridLines: { + display: true, + drawBorder: false, + }, + }, + { + id: "left-y-axis", + display: true, + type: "linear", + position: "left", + }, + { + id: "right-y-axis", + display: true, + type: "linear", + position: "right", + }, + ], + }, + responsive: true, + options: { + responsive: true, + maintainAspectRatio: true, + }, + plugins: { + legend: { + position: "bottom", + }, + }, +}; + +class MixedData extends React.Component { + constructor(props) { + super(props); + this.state = { + data: null, + }; + } + getData(chartData) { + var tempData = { + labels: [], + datasets: [], + }; + var tempdataSet = { + label: "", + type: "line", + borderColor: "#F3457E", + backgroundColor: "transparent", + yAxisID: "right-y-axis", + data: [], + dataSymbol: [], + order: 1, + }; + var tempdataSetTwo = { + label: "", + type: "bar", + backgroundColor: "#7B47A4", + yAxisID: "left-y-axis", + data: [], + dataSymbol: [], + order: 2, + }; + + for (var i = 0; i < chartData.length; i++) { + // Bar Data + if (i === 0) { + tempdataSetTwo.label = chartData[i].headerName; + for (var l = 0; l < chartData[i].plots.length; l++) { + tempData.labels.push(chartData[i].plots[l]["name"]); + tempdataSetTwo.data.push(chartData[i].plots[l]["value"]); + tempdataSetTwo.dataSymbol.push([ + chartData[i].plots[l]["symbol"], + "Unit", + ]); + } + } + // Line Data + if (i === 1) { + tempdataSet.label = chartData[i].headerName; + for (var j = 0; j < chartData[i].plots.length; j++) { + tempdataSet.data.push(chartData[i].plots[j]["value"]); + tempdataSet.dataSymbol.push([ + chartData[i].plots[j]["symbol"], + "Unit", + ]); + } + } + } + + tempData.datasets.push(tempdataSetTwo); + tempData.datasets.push(tempdataSet); + return tempData; + } + + render() { + let { chartData } = this.props; + let _data = this.getData(chartData); + if (_data) { + return ( + <Bar + data={_data} + plugins={[ + { + beforeInit: (chart, options) => { + chart.legend.afterFit = () => { + if (chart.legend.margins) { + chart.legend.options.labels.padding = 20; + } + }; + }, + }, + ]} + options={options} + /> + ); + } + return <div>Loading...</div>; + } +} + +export default MixedData; diff --git a/src/components/charts/NFormatterFun.js b/src/components/charts/NFormatterFun.js new file mode 100644 index 0000000000000000000000000000000000000000..1c168cfed4aa0b989d634fda8064e27fe5624024 --- /dev/null +++ b/src/components/charts/NFormatterFun.js @@ -0,0 +1,79 @@ +export default function NFormatterTest( + value, + type, + symbol, + commaSeparated = false +) { + var SI_SYMBOL = ["Unit", "Lac", "Cr"]; + const Rformatter = new Intl.NumberFormat("en-IN", { + // maximumFractionDigits:0, + useGrouping: true, + // currencyDisplay : Intl.NumberFormatOptions + //style: 'currency', + currency: "INR", + }); + // return value; + switch (type) { + case "amount": + case "Amount": + switch (symbol) { + case SI_SYMBOL[1]: + return ( + `${Rformatter.format((value / 100000).toFixed(2))}` || + `${Rformatter.format(0)}` + ); + + case SI_SYMBOL[2]: + return ( + `${Rformatter.format((value / 10000000).toFixed(2))}` || + `${Rformatter.format(0)}` + ); + + case SI_SYMBOL[0]: + if (value <= 9999999) { + return `${Rformatter.format(value || 0)}`; + } else { + if (!commaSeparated) { + return value; + // let nvalue = Rformatter.format((value).toFixed(2) || 0).replace('₹ ', ''); + // var right = nvalue.substring(nvalue.length - 12, nvalue.length); + // var left = nvalue.substring(0, nvalue.length - 12).replace(',', ''); + // let newVal = (left ? (left + ',') : '') + right; + // return parseFloat(newVal.replace(",,", ',').replace(',', '')); + } else { + let nvalue = Rformatter.format(value.toFixed(2) || 0).replace( + "₹ ", + "" + ); + var right = nvalue.substring(nvalue.length - 12, nvalue.length); + var left = nvalue + .substring(0, nvalue.length - 12) + .replace(",", ""); + let newVal = (left ? left + "," : "") + right; + return newVal.replace(",,", ","); + } + } + + default: + return parseFloat(`${Rformatter.format(value || 0)}`); + } + case "number": + case "Number": + if (!commaSeparated) { + return parseInt(value); + } + const Nformatter = new Intl.NumberFormat("en-IN"); + return Nformatter.format(Math.round(value)); + case "percentage": + case "Percentage": + const Pformatter = new Intl.NumberFormat("en-IN", { + maximumSignificantDigits: 3, + }); + return `${Pformatter.format(value)} %`; + case "text": + case "Text": + return value; + default: + return value; + } +} diff --git a/src/components/charts/PieChart.js b/src/components/charts/PieChart.js new file mode 100644 index 0000000000000000000000000000000000000000..1c75d715be584b219405fc7f6705fd87aaf06800 --- /dev/null +++ b/src/components/charts/PieChart.js @@ -0,0 +1,144 @@ +import React from "react"; +import { Pie } from "react-chartjs-2"; +import _ from "lodash"; +import piePalette from "./palette"; + +/** + * PieChart component + */ + +const pieChartOptions = { + options: { + responsive: true, + maintainAspectRatio: false, + }, + plugins: { + legend: { + position: "bottom", + display: true, + labels: { + font: { + family: "Lato-Regular", + size: 14, + lineHeight: 1.429, + }, + }, + }, + datalabels: { + anchor: "end", + align: "end", + }, + }, +}; + +class PieChart extends React.Component { + constructor(props) { + super(props); + this.state = { + data: null, + trigger: "", + }; + } + + /** + * Function to update the chart visualization + */ + updatePieVisuals = () => { + this.setState({ + trigger: true, + }); + this.props.pathName.history.push({ + pathName: "/dashboards", + state: { trigger: this.state.trigger }, + }); + setTimeout(() => { + this.props.pathName.history.push({ + pathName: "/dashboards", + state: { trigger: this.state.trigger }, + }); + }, 500); + }; + + getData(chartData) { + var pieChartTempData = { + labels: [], + datasets: [], + }; + var pieChartTempDataSet = { + label: "", + data: [], + dataSymbol: [], + }; + + _.map(chartData, function (k, v) { + var plots = k["plots"]; + for (var i = 0; i < plots.length; i++) { + pieChartTempData.labels.push(plots[i]["name"]); + pieChartTempDataSet.data.push(plots[i]["value"]); + pieChartTempDataSet.dataSymbol.push([plots[i]["symbol"], "Unit"]); + } + }); + + pieChartTempDataSet.backgroundColor = piePalette( + "cb-BasePalette", + pieChartTempDataSet.data.length + ).map(function (hex) { + return "#" + hex; + }); + pieChartTempDataSet.borderColor = piePalette( + "cb-BasePalette", + pieChartTempDataSet.data.length + ).map(function (hex) { + return "#" + hex; + }); + pieChartTempData.datasets.push(pieChartTempDataSet); + return pieChartTempData; + } + + render() { + let { chartData } = this.props; + let _data = this.getData(chartData); + + /* + * Function to get the chart label title + */ + const getPieLabelFilter = (elems) => { + if (localStorage.getItem("filterKey") && elems[0] !== undefined) { + let index = elems[0]._index; + let selectedLabel = { + labels: [], + }; + selectedLabel.labels.push(elems[0]._chart.data.labels[index]); + localStorage.setItem("label", selectedLabel.labels); + this.updatePieVisuals(); + } else { + // console.log("Out!"); + } + }; + + if (_data) { + return ( + <Pie + height={this.props.dimensions.height} + data={_data} + options={pieChartOptions} + plugins={[ + { + beforeInit: (chart, options) => { + chart.legend.afterFit = () => { + if (chart.legend.margins) { + chart.legend.options.labels.padding = 15; + } + }; + }, + }, + ]} + getElementAtEvent={(elems) => getPieLabelFilter(elems)} + /> + ); + } + return <div>Loading...</div>; + } +} + +export default PieChart; diff --git a/src/components/charts/WidgetNavBar.js b/src/components/charts/WidgetNavBar.js new file mode 100644 index 0000000000000000000000000000000000000000..c30ef8b18b804849f5333d4fcce1b55cd833d04c --- /dev/null +++ b/src/components/charts/WidgetNavBar.js @@ -0,0 +1,199 @@ +import React, { Component } from "react"; +import * as moment from "moment"; +import _ from "lodash"; +import "file-saver"; +// import domtoimage from "dom-to-image"; +import { ChartService } from "../../services"; + +/** + * Widget Navbar Component + * Holds all the widgets and drill throught filter labels + */ + +class WidgetNavBar extends Component { + constructor(props) { + super(props); + this.state = { + showDateFilter: false, + showCustomDateFilter: false, + dashboardConfigData: [], + selectedDate: moment().format("DD MMM YY"), + selectedFilter: "Today", + rangeSelected: "", + startDate: "", + endDate: "", + trigger: "", + selectedTab: "", + tabsInitDataId: [], + chartsGData: {}, + widgetData: [], + showOne: false, + }; + } + + componentDidMount() { + let thisMonthRange = + moment().startOf("month").format("DD MMM") + + " - " + + moment().endOf("month").format("DD MMM"); + this.setState({ + selectedFilter: "This month", + selectedDate: thisMonthRange, + }); + + ChartService.getDashboardConfig().then( + (response) => { + this.setState((prevState) => ({ + ...prevState, + dashboardConfigData: response.responseData, + })); + if (!this.state.chartsGData.length) { + setTimeout(() => this.getWidgets(), 150); + } + }, + (error) => {} + ); + } + + /** + * Function to get the chart data as per the dashboard selection + */ + getChartData = (code) => { + let startRange = moment().startOf("month"); + let endRange = moment().endOf("month"); + startRange = Number(startRange); + endRange = Number(endRange); + let thisMonth = { startDate: startRange, endDate: endRange }; + + let payload = { + RequestInfo: { + authToken: "null", + }, + headers: { + tenantId: "null", + }, + aggregationRequestDto: { + dashboardId: "home", + visualizationType: "METRIC", + visualizationCode: code, + queryType: "", + filters: {}, + moduleLevel: "", + aggregationFactors: null, + requestDate: thisMonth, + }, + }; + + ChartService.getChartData(payload).then( + (response) => { + this.setState( + (prevState) => ({ + ...prevState, + chartsGData: { + ...prevState.chartsGData, + [code]: response.responseData, + }, + }), + () => { + let chartDetails = JSON.stringify(this.state.chartsGData); + chartDetails = JSON.parse(chartDetails); + chartDetails = _.chain(chartDetails).map(); + chartDetails = JSON.stringify(chartDetails); + chartDetails = JSON.parse(chartDetails); + let chartData = []; + chartDetails.map((details) => chartData.push(details.data[0])); + this.setState({ + widgetData: [...chartData], + }); + } + ); + }, + (error) => {} + ); + }; + + /** + * Function to get the widgets data as per the dashboard selection + */ + getWidgets = () => { + let data = this.state.dashboardConfigData; + let dashboardWidget = _.chain(data) + .first() + .get("widgetCharts") + .groupBy("name") + .value(); + let widgetArray = _.chain(dashboardWidget).map(); + widgetArray = JSON.stringify(widgetArray); + widgetArray = JSON.parse(widgetArray); + let id = []; + widgetArray.map((code) => id.push(code[0].id)); + id.map((code) => this.getChartData(code)); + }; + + filterImage = (node) => { + return ( + node.id !== "downloadDashIcon" && + node.id !== "dropdownMenuButton" && + node.id !== "zoomIn" && + node.id !== "zoomOut" && + node.id !== "zoomInBtn" && + node.id !== "zoomOutBtn" + ); + }; + + /** + * Function to update the chart visualization + */ + updateVisuals = () => { + this.setState({ + trigger: true, + }); + this.props.pathName.history.push({ + pathName: "/dashboards", + state: { trigger: this.state.trigger }, + }); + setTimeout(() => { + this.props.pathName.history.push({ + pathName: "/dashboards", + state: { trigger: this.state.trigger }, + }); + }, 500); + }; + + render() { + return ( + <div className="mt-4"> + <div + className={`row col-12 ${ + this.state.widgetData && this.state.widgetData.length > 0 + ? "mt-4" + : "mt-0" + }`} + id="widgets" + > + {this.state.widgetData.map((data, index) => ( + <div + className="col-xs-12 col-sm-12 col-md-4 col-lg-4 col-xl-2 pb-3 pb-xs-3 pb-sm-3 pb-md-3 pb-lg-0 pb-xl-0 widget_card_one px-4" + key={data.headerName} + > + {(data.headerName || data.headerValue) && ( + <div className={`me-2 pt-3 widget-box-${index + 1}`}> + <h2 className="mt-3"> + {!data.isDecimal ? ( + <p>{Math.round(data.headerValue)}</p> + ) : ( + <p>{data.headerValue}</p> + )} + </h2> + <label className="pb-3">{data.headerName}</label> + </div> + )} + </div> + ))} + </div> + </div> + ); + } +} + +export default WidgetNavBar; diff --git a/src/components/charts/palette.js b/src/components/charts/palette.js new file mode 100644 index 0000000000000000000000000000000000000000..a3a8659f006edee0b77d753fd47aac84c167e60d --- /dev/null +++ b/src/components/charts/palette.js @@ -0,0 +1,1568 @@ +/* eslint-disable no-unused-expressions */ +/** @license + * + * Colour Palette Generator script. + * Copyright (c) 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may + * obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + * Furthermore, ColorBrewer colour schemes are covered by the following: + * + * Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and + * The Pennsylvania State University. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * 1. Redistributions as source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. The end-user documentation included with the redistribution, if any, + * must include the following acknowledgment: "This product includes color + * specifications and designs developed by Cynthia Brewer + * (http://colorbrewer.org/)." Alternately, this acknowledgment may appear + * in the software itself, if and wherever such third-party + * acknowledgments normally appear. + * + * 4. The name "ColorBrewer" must not be used to endorse or promote products + * derived from this software without prior written permission. For written + * permission, please contact Cynthia Brewer at cbrewer@psu.edu. + * + * 5. Products derived from this software may not be called "ColorBrewer", + * nor may "ColorBrewer" appear in their name, without prior written + * permission of Cynthia Brewer. + * + * Furthermore, Solarized colour schemes are covered by the following: + * + * Copyright (c) 2011 Ethan Schoonover + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +// 'use strict'; + + var palette = (function() { + + var proto = Array.prototype; + var slice = function(arr, opt_begin, opt_end) { + return proto.slice.apply(arr, proto.slice.call(arguments, 1)); + }; + + // var extend = function(arr, arr2) { + // return proto.push.apply(arr, arr2); + // }; + + var function_type = typeof function() {}; + + var INF = 1000000000; // As far as we're concerned, that's infinity. ;) + + + /** + * Generate a colour palette from given scheme. + * + * If scheme argument is not a function it is passed to palettes.listSchemes + * function (along with the number argument). This may result in an array + * of more than one available scheme. If that is the case, scheme at + * opt_index position is taken. + * + * This allows using different palettes for different data without having to + * name the schemes specifically, for example: + * + * palette_for_foo = palette('sequential', 10, 0); + * palette_for_bar = palette('sequential', 10, 1); + * palette_for_baz = palette('sequential', 10, 2); + * + * @param {!palette.SchemeType|string|palette.Palette} scheme Scheme to + * generate palette for. Either a function constructed with + * palette.Scheme object, or anything that palette.listSchemes accepts + * as name argument. + * @param {number} number Number of colours to return. If negative, absolute + * value is taken and colours will be returned in reverse order. + * @param {number=} opt_index If scheme is a name of a group or an array and + * results in more than one scheme, index of the scheme to use. The + * index wraps around. + * @param {...*} varargs Additional arguments to pass to palette or colour + * generator (if the chosen scheme uses those). + * @return {Array<string>} Array of abs(number) 'RRGGBB' strings or null if + * no matching scheme was found. + */ + var palette = function(scheme, number, opt_index, varargs) { + number |= 0; + if (number === 0) { + return []; + } + + if (typeof scheme !== function_type) { + var arr = palette.listSchemes( + /** @type {string|palette.Palette} */ (scheme), number); + if (!arr.length) { + return null; + } + scheme = arr[(opt_index || 0) % arr.length]; + } + + var args = slice(arguments, 2); + args[0] = number; + return scheme.apply(scheme, args); + }; + + + /** + * Returns a callable colour scheme object. + * + * Just after being created, the scheme has no colour palettes and no way of + * generating any, thus generate method will return null. To turn scheme + * into a useful object, addPalette, addPalettes or setColorFunction methods + * need to be used. + * + * To generate a colour palette with given number colours using function + * returned by this method, just call it with desired number of colours. + * + * Since this function *returns* a callable object, it must *not* be used + * with the new operator. + * + * @param {string} name Name of the scheme. + * @param {string|!Array<string>=} opt_groups A group name or list of + * groups the scheme should be categorised under. Three typical groups + * to use are 'qualitative', 'sequential' and 'diverging', but any + * groups may be created. + * @return {!palette.SchemeType} A colour palette generator function, which + * in addition has methods and properties like a regular object. Think + * of it as a callable object. + */ + palette.Scheme = function(name, opt_groups) { + /** + * A map from a number to a colour palettes with given number of colours. + * @type {!Object<number, palette.Palette>} + */ + var palettes = {}; + + /** + * The biggest palette in palettes map. + * @type {number} + */ + var palettes_max = 0; + + /** + * The smallest palette in palettes map. + * @type {number} + */ + var palettes_min = INF; + + var makeGenerator = function() { + if (arguments.length <= 1) { + return self.color_func.bind(self); + } else { + var args = slice(arguments); + return function(x) { + args[0] = x; + return self.color_func.apply(self, args); + }; + } + }; + + /** + * Generate a colour palette from the scheme. + * + * If there was a palette added with addPalette (or addPalettes) with + * enough colours, that palette will be used. Otherwise, if colour + * function has been set using setColorFunction method, that function will + * be used to generate the palette. Otherwise null is returned. + * + * @param {number} number Number of colours to return. If negative, + * absolute value is taken and colours will be returned in reverse + * order. + * @param {...*} varargs Additional arguments to pass to palette or colour + * generator (if the chosen scheme uses those). + */ + var self = function(number, varargs) { + number |= 0; + if (!number) { + return []; + } + + var _number = number; + number = Math.abs(number); + + if (number <= palettes_max) { + for (var i = Math.max(number, palettes_min); !(i in palettes); ++i) { + /* nop */ + } + var colors = palettes[i]; + if (i > number) { + var take_head = + 'shrinking_takes_head' in colors ? + colors.shrinking_takes_head : self.shrinking_takes_head; + if (take_head) { + colors = colors.slice(0, number); + i = number; + } else { + return palette.generate( + function(x) { return colors[Math.round(x)]; }, + _number, 0, colors.length - 1); + } + } + colors = colors.slice(); + if (_number < 0) { + colors.reverse(); + } + return colors; + + } else if (self.color_func) { + return palette.generate(makeGenerator.apply(self, arguments), + _number, 0, 1, self.color_func_cyclic); + + } else { + return null; + } + }; + + /** + * The name of the palette. + * @type {string} + */ + self.scheme_name = name; + + /** + * A list of groups the palette belongs to. + * @type {!Array<string>} + */ + self.groups = opt_groups ? + typeof opt_groups === 'string' ? [opt_groups] : opt_groups : []; + + /** + * The biggest palette this scheme can generate. + * @type {number} + */ + self.max = 0; + + /** + * The biggest palette this scheme can generate that is colour-blind + * friendly. + * @type {number} + */ + self.cbf_max = INF; + + + /** + * Adds a colour palette to the colour scheme. + * + * @param {palette.Palette} palette An array of 'RRGGBB' strings + * representing the palette to add. + * @param {boolean=} opt_is_cbf Whether the palette is colourblind friendly. + */ + self.addPalette = function(palette, opt_is_cbf) { + var len = palette.length; + if (len) { + palettes[len] = palette; + palettes_min = Math.min(palettes_min, len); + palettes_max = Math.max(palettes_max, len); + self.max = Math.max(self.max, len); + if (!opt_is_cbf && len !== 1) { + self.cbf_max = Math.min(self.cbf_max, len - 1); + } + } + }; + + /** + * Adds number of colour palettes to the colour scheme. + * + * @param {palette.PalettesList} palettes A map or an array of colour + * palettes to add. If map, i.e. object, is used, properties should + * use integer property names. + * @param {number=} opt_max Size of the biggest palette in palettes set. + * If not set, palettes must have a length property which will be used. + * @param {number=} opt_cbf_max Size of the biggest palette which is still + * colourblind friendly. 1 by default. + */ + self.addPalettes = function(palettes, opt_max, opt_cbf_max) { + opt_max = opt_max || palettes.length; + for (var i = 0; i < opt_max; ++i) { + if (i in palettes) { + self.addPalette(palettes[i], true); + } + } + self.cbf_max = Math.min(self.cbf_max, opt_cbf_max || 1); + }; + + /** + * Enable shrinking palettes taking head of the list of colours. + * + * When user requests n-colour palette but the smallest palette added with + * addPalette (or addPalettes) is m-colour one (where n < m), n colours + * across the palette will be returned. For example: + * var ex = palette.Scheme('ex'); + * ex.addPalette(['000000', 'bcbcbc', 'ffffff']); + * var pal = ex(2); + * // pal == ['000000', 'ffffff'] + * + * This works for palettes where the distance between colours is + * correlated to distance in the palette array, which is true in gradients + * such as the one above. + * + * To turn this feature off shrinkByTakingHead can be set to true either + * for all palettes in the scheme (if opt_idx is not given) or for palette + * with given number of colours only. In general, setting the option for + * given palette overwrites whatever has been set for the scheme. The + * default, as described above, is false. + * + * Alternatively, the feature can be enabled by setting shrinking_takes_head + * property for the palette Array or the scheme object. + * + * For example, all of the below give equivalent results: + * var pal = ['ff0000', '00ff00', '0000ff']; + * + * var ex = palette.Scheme('ex'); + * ex.addPalette(pal); // ex(2) == ['ff0000', '0000ff'] + * ex.shrinkByTakingHead(true); // ex(2) == ['ff0000', '00ff00'] + * + * ex = palette.Scheme('ex'); + * ex.addPalette(pal); // ex(2) == ['ff0000', '0000ff'] + * ex.shrinkByTakingHead(true, 3); // ex(2) == ['ff0000', '00ff00'] + * + * ex = palette.Scheme('ex'); + * ex.addPalette(pal); + * ex.addPalette(pal); // ex(2) == ['ff0000', '0000ff'] + * pal.shrinking_takes_head = true; // ex(2) == ['ff0000', '00ff00'] + * + * @param {boolean} enabled Whether to enable or disable the “shrinking + * takes head†feature. It is disabled by default. + * @param {number=} opt_idx If given, the “shrinking takes head†option + * for palette with given number of colours is set. If such palette + * does not exist, nothing happens. + */ + self.shrinkByTakingHead = function(enabled, opt_idx) { + if (opt_idx !== void(0)) { + if (opt_idx in palettes) { + palettes[opt_idx].shrinking_takes_head = !!enabled; + } + } else { + self.shrinking_takes_head = !!enabled; + } + }; + + /** + * Sets a colour generation function of the colour scheme. + * + * The function must accept a singe number argument whose value can be from + * 0.0 to 1.0, and return a colour as an 'RRGGBB' string. This function + * will be used when generating palettes, i.e. if 11-colour palette is + * requested, this function will be called with arguments 0.0, 0.1, …, 1.0. + * + * If the palette generated by the function is colourblind friendly, + * opt_is_cbf should be set to true. + * + * In some cases, it is not desirable to reach 1.0 when generating + * a palette. This happens for hue-rainbows where the 0–1 range corresponds + * to a 0°–360° range in hues, and since hue at 0° is the same as at 360°, + * it's desired to stop short the end of the range when generating + * a palette. To accomplish this, opt_cyclic should be set to true. + * + * @param {palette.ColorFunction} func A colour generator function. + * @param {boolean=} opt_is_cbf Whether palette generate with the function + * is colour-blind friendly. + * @param {boolean=} opt_cyclic Whether colour at 0.0 is the same as the + * one at 1.0. + */ + self.setColorFunction = function(func, opt_is_cbf, opt_cyclic) { + self.color_func = func; + self.color_func_cyclic = !!opt_cyclic; + self.max = INF; + if (!opt_is_cbf && self.cbf_max === INF) { + self.cbf_max = 1; + } + }; + + self.color = function(x, varargs) { + if (self.color_func) { + return self.color_func.apply(this, arguments); + } else { + return null; + } + }; + + return self; + }; + + + /** + * Creates a new palette.Scheme and initialises it by calling addPalettes + * method with the rest of the arguments. + * + * @param {string} name Name of the scheme. + * @param {string|!Array<string>} groups A group name or list of group + * names the scheme belongs to. + * @param {!Object<number, palette.Palette>|!Array<palette.Palette>} + * palettes A map or an array of colour palettes to add. If map, i.e. + * object, is used, properties should use integer property names. + * @param {number=} opt_max Size of the biggest palette in palettes set. + * If not set, palettes must have a length property which will be used. + * @param {number=} opt_cbf_max Size of the biggest palette which is still + * colourblind friendly. 1 by default. + * @return {!palette.SchemeType} A colour palette generator function, which + * in addition has methods and properties like a regular object. Think + * of it as a callable object. + */ + palette.Scheme.fromPalettes = function(name, groups, + palettes, opt_max, opt_cbf_max) { + var scheme = palette.Scheme(name, groups); + scheme.addPalettes.apply(scheme, slice(arguments, 2)); + return scheme; + }; + + + /** + * Creates a new palette.Scheme and initialises it by calling + * setColorFunction method with the rest of the arguments. + * + * @param {string} name Name of the scheme. + * @param {string|!Array<string>} groups A group name or list of group + * names the scheme belongs to. + * @param {palette.ColorFunction} func A colour generator function. + * @param {boolean=} opt_is_cbf Whether palette generate with the function + * is colour-blind friendly. + * @param {boolean=} opt_cyclic Whether colour at 0.0 is the same as the + * one at 1.0. + * @return {!palette.SchemeType} A colour palette generator function, which + * in addition has methods and properties like a regular object. Think + * of it as a callable object. + */ + palette.Scheme.withColorFunction = function(name, groups, + func, opt_is_cbf, opt_cyclic) { + var scheme = palette.Scheme(name, groups); + scheme.setColorFunction.apply(scheme, slice(arguments, 2)); + return scheme; + }; + + + /** + * A map of registered schemes. Maps a scheme or group name to a list of + * scheme objects. Property name is either 'n-<name>' for single scheme + * names or 'g-<name>' for scheme group names. + * + * @type {!Object<string, !Array<!Object>>} + */ + var registered_schemes = {}; + + + /** + * Registers a new colour scheme. + * + * @param {!palette.SchemeType} scheme The scheme to add. + */ + palette.register = function(scheme) { + registered_schemes['n-' + scheme.scheme_name] = [scheme]; + scheme.groups.forEach(function(g) { + (registered_schemes['g-' + g] = + registered_schemes['g-' + g] || []).push(scheme); + }); + (registered_schemes['g-all'] = + registered_schemes['g-all'] || []).push(scheme); + }; + + + /** + * List all schemes that match given name and number of colours. + * + * name argument can be either a string or an array of strings. In the + * former case, the function acts as if the argument was an array with name + * as a single argument (i.e. “palette.listSchemes('foo')†is exactly the same + * as “palette.listSchemes(['foo'])â€). + * + * Each name can be either name of a palette (e.g. 'tol-sq' for Paul Tol's + * sequential palette), or a name of a group (e.g. 'sequential' for all + * sequential palettes). Name can therefore map to a single scheme or + * several schemes. + * + * Furthermore, name can be suffixed with '-cbf' to indicate that only + * schemes that are colourblind friendly should be returned. For example, + * 'rainbow' returns a HSV rainbow scheme, but because it is not colourblind + * friendly, 'rainbow-cbf' returns no schemes. + * + * Some schemes may produce colourblind friendly palettes for some number of + * colours. For example ColorBrewer's Dark2 scheme is colourblind friendly + * if no more than 3 colours are generated. If opt_number is not specified, + * 'qualitative-cbf' will include 'cb-Dark2' but if opt_number is given as, + * say, 5 it won't. + * + * Name can also be 'all' which will return all registered schemes. + * Naturally, 'all-cbf' will return all colourblind friendly schemes. + * + * Schemes are added to the library using palette.register. Schemes are + * created using palette.Scheme function. By default, the following schemes + * are available: + * + * Name Description + * -------------- ----------------------------------------------------- + * tol Paul Tol's qualitative scheme, cbf, max 12 colours. + * tol-dv Paul Tol's diverging scheme, cbf. + * tol-sq Paul Tol's sequential scheme, cbf. + * tol-rainbow Paul Tol's qualitative scheme, cbf. + * + * rainbow A rainbow palette. + * + * cb-YlGn ColorBrewer sequential schemes. + * cb-YlGnBu + * cb-GnBu + * cb-BuGn + * cb-PuBuGn + * cb-PuBu + * cb-BuPu + * cb-RdPu + * cb-PuRd + * cb-OrRd + * cb-YlOrRd + * cb-YlOrBr + * cb-Purples + * cb-Blues + * cb-Greens + * cb-Oranges + * cb-Reds + * cb-Greys + * + * cb-PuOr ColorBrewer diverging schemes. + * cb-BrBG + * cb-PRGn + * cb-PiYG + * cb-RdBu + * cb-RdGy + * cb-RdYlBu + * cb-Spectral + * cb-RdYlGn + * + * cb-Accent ColorBrewer qualitative schemes. + * cb-Dark2 + * cb-Paired + * cb-Pastel1 + * cb-Pastel2 + * cb-Set1 + * cb-Set2 + * cb-Set3 + * + * sol-base Solarized base colours. + * sol-accent Solarized accent colours. + * + * The following groups are also available by default: + * + * Name Description + * -------------- ----------------------------------------------------- + * all All registered schemes. + * sequential All sequential schemes. + * diverging All diverging schemes. + * qualitative All qualitative schemes. + * cb-sequential All ColorBrewer sequential schemes. + * cb-diverging All ColorBrewer diverging schemes. + * cb-qualitative All ColorBrewer qualitative schemes. + * + * You can read more about Paul Tol's palettes at http://www.sron.nl/~pault/. + * You can read more about ColorBrewer at http://colorbrewer2.org. + * + * @param {string|!Array<string>} name A name of a colour scheme, of + * a group of colour schemes, or an array of any of those. + * @param {number=} opt_number When requesting only colourblind friendly + * schemes, number of colours the scheme must provide generating such + * that the palette is still colourblind friendly. 2 by default. + * @return {!Array<!palette.SchemeType>} An array of colour scheme objects + * matching the criteria. Sorted by scheme name. + */ + palette.listSchemes = function(name, opt_number) { + if (!opt_number) { + opt_number = 2; + } else if (opt_number < 0) { + opt_number = -opt_number; + } + + var ret = []; + (typeof name === 'string' ? [name] : name).forEach(function(n) { + var cbf = n.substring(n.length - 4) === '-cbf'; + if (cbf) { + n = n.substring(0, n.length - 4); + } + var schemes = + registered_schemes['g-' + n] || + registered_schemes['n-' + n] || + []; + for (var i = 0, scheme; (scheme = schemes[i]); ++i) { + if ((cbf ? scheme.cbf : scheme.max) >= opt_number) { + ret.push(scheme); + } + } + }); + + ret.sort(function(a, b) { + return a.scheme_name >= b.scheme_name ? + a.scheme_name > b.scheme_name ? 1 : 0 : -1; + }); + return ret; + }; + + + /** + * Generates a palette using given colour generating function. + * + * The color_func callback must accept a singe number argument whose value + * can vary from 0.0 to 1.0 (or in general from opt_start to opt_end), and + * return a colour as an 'RRGGBB' string. This function will be used when + * generating palettes, i.e. if 11-colour palette is requested, this + * function will be called with arguments 0.0, 0.1, …, 1.0. + * + * In some cases, it is not desirable to reach 1.0 when generating + * a palette. This happens for hue-rainbows where the 0–1 range corresponds + * to a 0°–360° range in hues, and since hue at 0° is the same as at 360°, + * it's desired to stop short the end of the range when generating + * a palette. To accomplish this, opt_cyclic should be set to true. + * + * opt_start and opt_end may be used to change the range the colour + * generation function is called with. opt_end may be less than opt_start + * which will case to traverse the range in reverse. Another way to reverse + * the palette is requesting negative number of colours. The two methods do + * not always lead to the same results (especially if opt_cyclic is set). + * + * @param {palette.ColorFunction} color_func A colours generating callback + * function. + * @param {number} number Number of colours to generate in the palette. If + * number is negative, colours in the palette will be reversed. If only + * one colour is requested, colour at opt_start will be returned. + * @param {number=} opt_start Optional starting point for the palette + * generation function. Zero by default. + * @param {number=} opt_end Optional ending point for the palette generation + * function. One by default. + * @param {boolean=} opt_cyclic If true, function will assume colour at + * point opt_start is the same as one at opt_end. + * @return {palette.Palette} An array of 'RRGGBB' colours. + */ + palette.generate = function(color_func, number, opt_start, opt_end, + opt_cyclic) { + if (Math.abs(number) < 1) { + return []; + } + + opt_start = opt_start === void(0) ? 0 : opt_start; + opt_end = opt_end === void(0) ? 1 : opt_end; + + if (Math.abs(number) < 2) { + return [color_func(opt_start)]; + } + + var i = Math.abs(number); + var x = opt_start; + var ret = []; + var step = (opt_end - opt_start) / (opt_cyclic ? i : (i - 1)); + + for (; --i >= 0; x += step) { + ret.push(color_func(x)); + } + if (number < 0) { + ret.reverse(); + } + return ret; + }; + + + /** + * Clamps value to [0, 1] range. + * @param {number} v Number to limit value of. + * @return {number} If v is inside of [0, 1] range returns v, otherwise + * returns 0 or 1 depending which side of the range v is closer to. + */ + var clamp = function(v) { + return v > 0 ? (v < 1 ? v : 1) : 0; + }; + + /** + * Converts r, g, b triple into RRGGBB hex representation. + * @param {number} r Red value of the colour in the range [0, 1]. + * @param {number} g Green value of the colour in the range [0, 1]. + * @param {number} b Blue value of the colour in the range [0, 1]. + * @return {string} A lower-case RRGGBB representation of the colour. + */ + palette.rgbColor = function(r, g, b) { + return [r, g, b].map(function(v) { + v = Number(Math.round(clamp(v) * 255)).toString(16); + return v.length === 1 ? '0' + v : v; + }).join(''); + }; + + /** + * Converts a linear r, g, b triple into RRGGBB hex representation. + * @param {number} r Linear red value of the colour in the range [0, 1]. + * @param {number} g Linear green value of the colour in the range [0, 1]. + * @param {number} b Linear blue value of the colour in the range [0, 1]. + * @return {string} A lower-case RRGGBB representation of the colour. + */ + palette.linearRgbColor = function(r, g, b) { + // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + return [r, g, b].map(function(v) { + v = clamp(v); + if (v <= 0.0031308) { + v = 12.92 * v; + } else { + v = 1.055 * Math.pow(v, 1 / 2.4) - 0.055; + } + v = Number(Math.round(v * 255)).toString(16); + return v.length === 1 ? '0' + v : v; + }).join(''); + }; + + /** + * Converts an HSV colours to RRGGBB hex representation. + * @param {number} h Hue in the range [0, 1]. + * @param {number=} opt_s Saturation in the range [0, 1]. One by default. + * @param {number=} opt_v Value in the range [0, 1]. One by default. + * @return {string} An RRGGBB representation of the colour. + */ + palette.hsvColor = function(h, opt_s, opt_v) { + h *= 6; + var s = opt_s === void(0) ? 1 : clamp(opt_s); + var v = opt_v === void(0) ? 1 : clamp(opt_v); + var x = v * (1 - s * Math.abs(h % 2 - 1)); + var m = v * (1 - s); + switch (Math.floor(h) % 6) { + case 0: return palette.rgbColor(v, x, m); + case 1: return palette.rgbColor(x, v, m); + case 2: return palette.rgbColor(m, v, x); + case 3: return palette.rgbColor(m, x, v); + case 4: return palette.rgbColor(x, m, v); + default: return palette.rgbColor(v, m, x); + } + }; + + palette.register(palette.Scheme.withColorFunction( + 'rainbow', 'qualitative', palette.hsvColor, false, true)); + + return palette; + })(); + + + /** @typedef {function(number): string} */ + palette.ColorFunction; + + /** @typedef {!Array<string>} */ + palette.Palette; + + /** @typedef {!Object<number, palette.Palette>|!Array<palette.Palette>} */ + palette.PalettesList; + + /** + * @typedef { + * function(number, ...?): Array<string>| + * { + * scheme_name: string, + * groups: !Array<string>, + * max: number, + * cbf_max: number, + * addPalette: function(!Array<string>, boolean=), + * addPalettes: function(palette.PalettesList, number=, number=), + * shrinkByTakingHead: function(boolean, number=), + * setColorFunction: function(palette.ColorFunction, boolean=, boolean=), + * color: function(number, ...?): ?string}} + */ + palette.SchemeType; + + + /* mpn65 palette start here. ************************************************/ + + /* The ‘mpn65’ palette is designed for systems which show many graphs which + don’t have custom colour palettes chosen by humans or if number of necessary + colours isn’t know a priori. */ + + (function() { + var scheme = palette.Scheme.fromPalettes('mpn65', 'qualitative', [[ + 'ff0029', '377eb8', '66a61e', '984ea3', '00d2d5', 'ff7f00', 'af8d00', + '7f80cd', 'b3e900', 'c42e60', 'a65628', 'f781bf', '8dd3c7', 'bebada', + 'fb8072', '80b1d3', 'fdb462', 'fccde5', 'bc80bd', 'ffed6f', 'c4eaff', + 'cf8c00', '1b9e77', 'd95f02', 'e7298a', 'e6ab02', 'a6761d', '0097ff', + '00d067', '000000', '252525', '525252', '737373', '969696', 'bdbdbd', + 'f43600', '4ba93b', '5779bb', '927acc', '97ee3f', 'bf3947', '9f5b00', + 'f48758', '8caed6', 'f2b94f', 'eff26e', 'e43872', 'd9b100', '9d7a00', + '698cff', 'd9d9d9', '00d27e', 'd06800', '009f82', 'c49200', 'cbe8ff', + 'fecddf', 'c27eb6', '8cd2ce', 'c4b8d9', 'f883b0', 'a49100', 'f48800', + '27d0df', 'a04a9b' + ]]); + scheme.shrinkByTakingHead(true); + palette.register(scheme); + })(); + + /* Paul Tol's schemes start here. *******************************************/ + /* See http://www.sron.nl/~pault/ */ + + (function() { + var rgb = palette.rgbColor; + + /** + * Calculates value of a polynomial at given point. + * For example, poly(x, 1, 2, 3) calculates value of “1 + 2*x + 3*X²â€. + * @param {number} x Value to calculate polynomial for. + * @param {...number} varargs Coefficients of the polynomial specified in + * the order of rising powers of x including constant as the first + * variable argument. + */ + var poly = function(x, varargs) { + var i = arguments.length - 1, n = arguments[i]; + while (i > 1) { + n = n * x + arguments[--i]; + } + return n; + }; + + /** + * Calculate approximate value of error function with maximum error of 0.0005. + * See <https://en.wikipedia.org/wiki/Error_function>. + * @param {number} x Argument of the error function. + * @return {number} Value of error function for x. + */ + var erf = function(x) { + // https://en.wikipedia.org/wiki/Error_function#Approximation_with_elementary_functions + // This produces a maximum error of 0.0005 which is more then we need. In + // the worst case, that error is multiplied by four and then divided by two + // before being multiplied by 255, so in the end, the error is multiplied by + // 510 which produces 0.255 which is less than a single colour step. + var y = poly(Math.abs(x), 1, 0.278393, 0.230389, 0.000972, 0.078108); + y *= y; // y^2 + y *= y; // y^4 + y = 1 - 1 / y; + return x < 0 ? -y : y; + }; + + palette.register(palette.Scheme.fromPalettes('tol', 'qualitative', [ + ['4477aa'], + ['4477aa', 'cc6677'], + ['4477aa', 'ddcc77', 'cc6677'], + ['4477aa', '117733', 'ddcc77', 'cc6677'], + ['332288', '88ccee', '117733', 'ddcc77', 'cc6677'], + ['332288', '88ccee', '117733', 'ddcc77', 'cc6677', 'aa4499'], + ['332288', '88ccee', '44aa99', '117733', 'ddcc77', 'cc6677', 'aa4499'], + ['332288', '88ccee', '44aa99', '117733', '999933', 'ddcc77', 'cc6677', + 'aa4499'], + ['332288', '88ccee', '44aa99', '117733', '999933', 'ddcc77', 'cc6677', + '882255', 'aa4499'], + ['332288', '88ccee', '44aa99', '117733', '999933', 'ddcc77', '661100', + 'cc6677', '882255', 'aa4499'], + ['332288', '6699cc', '88ccee', '44aa99', '117733', '999933', 'ddcc77', + '661100', 'cc6677', '882255', 'aa4499'], + ['332288', '6699cc', '88ccee', '44aa99', '117733', '999933', 'ddcc77', + '661100', 'cc6677', 'aa4466', '882255', 'aa4499'] + ], 12, 12)); + + /** + * Calculates a colour along Paul Tol's sequential colours axis. + * See <http://www.sron.nl/~pault/colourschemes.pdf> figure 7 and equation 1. + * @param {number} x Position of the colour on the axis in the [0, 1] range. + * @return {string} An RRGGBB representation of the colour. + */ + palette.tolSequentialColor = function(x) { + return rgb(1 - 0.392 * (1 + erf((x - 0.869) / 0.255)), + 1.021 - 0.456 * (1 + erf((x - 0.527) / 0.376)), + 1 - 0.493 * (1 + erf((x - 0.272) / 0.309))); + }; + + palette.register(palette.Scheme.withColorFunction( + 'tol-sq', 'sequential', palette.tolSequentialColor, true)); + + /** + * Calculates a colour along Paul Tol's diverging colours axis. + * See <http://www.sron.nl/~pault/colourschemes.pdf> figure 8 and equation 2. + * @param {number} x Position of the colour on the axis in the [0, 1] range. + * @return {string} An RRGGBB representation of the colour. + */ + palette.tolDivergingColor = function(x) { + var g = poly(x, 0.572, 1.524, -1.811) / poly(x, 1, -0.291, 0.1574); + return rgb(poly(x, 0.235, -2.13, 26.92, -65.5, 63.5, -22.36), + g * g, + 1 / poly(x, 1.579, -4.03, 12.92, -31.4, 48.6, -23.36)); + }; + + palette.register(palette.Scheme.withColorFunction( + 'tol-dv', 'diverging', palette.tolDivergingColor, true)); + + /** + * Calculates a colour along Paul Tol's rainbow colours axis. + * See <http://www.sron.nl/~pault/colourschemes.pdf> figure 13 and equation 3. + * @param {number} x Position of the colour on the axis in the [0, 1] range. + * @return {string} An RRGGBB representation of the colour. + */ + palette.tolRainbowColor = function(x) { + return rgb(poly(x, 0.472, -0.567, 4.05) / poly(x, 1, 8.72, -19.17, 14.1), + poly(x, 0.108932, -1.22635, 27.284, -98.577, 163.3, -131.395, + 40.634), + 1 / poly(x, 1.97, 3.54, -68.5, 243, -297, 125)); + }; + + palette.register(palette.Scheme.withColorFunction( + 'tol-rainbow', 'qualitative', palette.tolRainbowColor, true)); + })(); + + + /* Solarized colour schemes start here. *************************************/ + /* See http://ethanschoonover.com/solarized */ + + (function() { + /* + * Those are not really designed to be used in graphs, but we're keeping + * them here in case someone cares. + */ + palette.register(palette.Scheme.fromPalettes('sol-base', 'sequential', [ + ['002b36', '073642', '586e75', '657b83', '839496', '93a1a1', 'eee8d5', + 'fdf6e3'] + ], 1, 8)); + palette.register(palette.Scheme.fromPalettes('sol-accent', 'qualitative', [ + ['b58900', 'cb4b16', 'dc322f', 'd33682', '6c71c4', '268bd2', '2aa198', + '859900'] + ])); + })(); + + + /* ColorBrewer colour schemes start here. ***********************************/ + /* See http://colorbrewer2.org/ */ + + (function() { + var schemes = { + YlGn: { + type: 'sequential', + cbf: 42, + 3: ['f7fcb9', 'addd8e', '31a354'], + 4: ['ffffcc', 'c2e699', '78c679', '238443'], + 5: ['ffffcc', 'c2e699', '78c679', '31a354', '006837'], + 6: ['ffffcc', 'd9f0a3', 'addd8e', '78c679', '31a354', '006837'], + 7: ['ffffcc', 'd9f0a3', 'addd8e', '78c679', '41ab5d', '238443', + '005a32'], + 8: ['ffffe5', 'f7fcb9', 'd9f0a3', 'addd8e', '78c679', '41ab5d', + '238443', '005a32'], + 9: ['ffffe5', 'f7fcb9', 'd9f0a3', 'addd8e', '78c679', '41ab5d', + '238443', '006837', '004529'] + }, + YlGnBu: { + type: 'sequential', + cbf: 42, + 3: ['edf8b1', '7fcdbb', '2c7fb8'], + 4: ['ffffcc', 'a1dab4', '41b6c4', '225ea8'], + 5: ['ffffcc', 'a1dab4', '41b6c4', '2c7fb8', '253494'], + 6: ['ffffcc', 'c7e9b4', '7fcdbb', '41b6c4', '2c7fb8', '253494'], + 7: ['ffffcc', 'c7e9b4', '7fcdbb', '41b6c4', '1d91c0', '225ea8', + '0c2c84'], + 8: ['ffffd9', 'edf8b1', 'c7e9b4', '7fcdbb', '41b6c4', '1d91c0', + '225ea8', '0c2c84'], + 9: ['ffffd9', 'edf8b1', 'c7e9b4', '7fcdbb', '41b6c4', '1d91c0', + '225ea8', '253494', '081d58'] + }, + GnBu: { + type: 'sequential', + cbf: 42, + 3: ['e0f3db', 'a8ddb5', '43a2ca'], + 4: ['f0f9e8', 'bae4bc', '7bccc4', '2b8cbe'], + 5: ['f0f9e8', 'bae4bc', '7bccc4', '43a2ca', '0868ac'], + 6: ['f0f9e8', 'ccebc5', 'a8ddb5', '7bccc4', '43a2ca', '0868ac'], + 7: ['f0f9e8', 'ccebc5', 'a8ddb5', '7bccc4', '4eb3d3', '2b8cbe', + '08589e'], + 8: ['f7fcf0', 'e0f3db', 'ccebc5', 'a8ddb5', '7bccc4', '4eb3d3', + '2b8cbe', '08589e'], + 9: ['f7fcf0', 'e0f3db', 'ccebc5', 'a8ddb5', '7bccc4', '4eb3d3', + '2b8cbe', '0868ac', '084081'] + }, + BuGn: { + type: 'sequential', + cbf: 42, + 3: ['e5f5f9', '99d8c9', '2ca25f'], + 4: ['edf8fb', 'b2e2e2', '66c2a4', '238b45'], + 5: ['edf8fb', 'b2e2e2', '66c2a4', '2ca25f', '006d2c'], + 6: ['edf8fb', 'ccece6', '99d8c9', '66c2a4', '2ca25f', '006d2c'], + 7: ['edf8fb', 'ccece6', '99d8c9', '66c2a4', '41ae76', '238b45', + '005824'], + 8: ['f7fcfd', 'e5f5f9', 'ccece6', '99d8c9', '66c2a4', '41ae76', + '238b45', '005824'], + 9: ['f7fcfd', 'e5f5f9', 'ccece6', '99d8c9', '66c2a4', '41ae76', + '238b45', '006d2c', '00441b'] + }, + PuBuGn: { + type: 'sequential', + cbf: 42, + 3: ['ece2f0', 'a6bddb', '1c9099'], + 4: ['f6eff7', 'bdc9e1', '67a9cf', '02818a'], + 5: ['f6eff7', 'bdc9e1', '67a9cf', '1c9099', '016c59'], + 6: ['f6eff7', 'd0d1e6', 'a6bddb', '67a9cf', '1c9099', '016c59'], + 7: ['f6eff7', 'd0d1e6', 'a6bddb', '67a9cf', '3690c0', '02818a', + '016450'], + 8: ['fff7fb', 'ece2f0', 'd0d1e6', 'a6bddb', '67a9cf', '3690c0', + '02818a', '016450'], + 9: ['fff7fb', 'ece2f0', 'd0d1e6', 'a6bddb', '67a9cf', '3690c0', + '02818a', '016c59', '014636'] + }, + PuBu: { + type: 'sequential', + cbf: 42, + 3: ['ece7f2', 'a6bddb', '2b8cbe'], + 4: ['f1eef6', 'bdc9e1', '74a9cf', '0570b0'], + 5: ['f1eef6', 'bdc9e1', '74a9cf', '2b8cbe', '045a8d'], + 6: ['f1eef6', 'd0d1e6', 'a6bddb', '74a9cf', '2b8cbe', '045a8d'], + 7: ['f1eef6', 'd0d1e6', 'a6bddb', '74a9cf', '3690c0', '0570b0', + '034e7b'], + 8: ['fff7fb', 'ece7f2', 'd0d1e6', 'a6bddb', '74a9cf', '3690c0', + '0570b0', '034e7b'], + 9: ['fff7fb', 'ece7f2', 'd0d1e6', 'a6bddb', '74a9cf', '3690c0', + '0570b0', '045a8d', '023858'] + }, + BuPu: { + type: 'sequential', + cbf: 42, + 3: ['e0ecf4', '9ebcda', '8856a7'], + 4: ['edf8fb', 'b3cde3', '8c96c6', '88419d'], + 5: ['edf8fb', 'b3cde3', '8c96c6', '8856a7', '810f7c'], + 6: ['edf8fb', 'bfd3e6', '9ebcda', '8c96c6', '8856a7', '810f7c'], + 7: ['edf8fb', 'bfd3e6', '9ebcda', '8c96c6', '8c6bb1', '88419d', + '6e016b'], + 8: ['f7fcfd', 'e0ecf4', 'bfd3e6', '9ebcda', '8c96c6', '8c6bb1', + '88419d', '6e016b'], + 9: ['f7fcfd', 'e0ecf4', 'bfd3e6', '9ebcda', '8c96c6', '8c6bb1', + '88419d', '810f7c', '4d004b'] + }, + RdPu: { + type: 'sequential', + cbf: 42, + 3: ['fde0dd', 'fa9fb5', 'c51b8a'], + 4: ['feebe2', 'fbb4b9', 'f768a1', 'ae017e'], + 5: ['feebe2', 'fbb4b9', 'f768a1', 'c51b8a', '7a0177'], + 6: ['feebe2', 'fcc5c0', 'fa9fb5', 'f768a1', 'c51b8a', '7a0177'], + 7: ['feebe2', 'fcc5c0', 'fa9fb5', 'f768a1', 'dd3497', 'ae017e', + '7a0177'], + 8: ['fff7f3', 'fde0dd', 'fcc5c0', 'fa9fb5', 'f768a1', 'dd3497', + 'ae017e', '7a0177'], + 9: ['fff7f3', 'fde0dd', 'fcc5c0', 'fa9fb5', 'f768a1', 'dd3497', + 'ae017e', '7a0177', '49006a'] + }, + PuRd: { + type: 'sequential', + cbf: 42, + 3: ['e7e1ef', 'c994c7', 'dd1c77'], + 4: ['f1eef6', 'd7b5d8', 'df65b0', 'ce1256'], + 5: ['f1eef6', 'd7b5d8', 'df65b0', 'dd1c77', '980043'], + 6: ['f1eef6', 'd4b9da', 'c994c7', 'df65b0', 'dd1c77', '980043'], + 7: ['f1eef6', 'd4b9da', 'c994c7', 'df65b0', 'e7298a', 'ce1256', + '91003f'], + 8: ['f7f4f9', 'e7e1ef', 'd4b9da', 'c994c7', 'df65b0', 'e7298a', + 'ce1256', '91003f'], + 9: ['f7f4f9', 'e7e1ef', 'd4b9da', 'c994c7', 'df65b0', 'e7298a', + 'ce1256', '980043', '67001f'] + }, + OrRd: { + type: 'sequential', + cbf: 42, + 3: ['fee8c8', 'fdbb84', 'e34a33'], + 4: ['fef0d9', 'fdcc8a', 'fc8d59', 'd7301f'], + 5: ['fef0d9', 'fdcc8a', 'fc8d59', 'e34a33', 'b30000'], + 6: ['fef0d9', 'fdd49e', 'fdbb84', 'fc8d59', 'e34a33', 'b30000'], + 7: ['fef0d9', 'fdd49e', 'fdbb84', 'fc8d59', 'ef6548', 'd7301f', + '990000'], + 8: ['fff7ec', 'fee8c8', 'fdd49e', 'fdbb84', 'fc8d59', 'ef6548', + 'd7301f', '990000'], + 9: ['fff7ec', 'fee8c8', 'fdd49e', 'fdbb84', 'fc8d59', 'ef6548', + 'd7301f', 'b30000', '7f0000'] + }, + YlOrRd: { + type: 'sequential', + cbf: 42, + 3: ['ffeda0', 'feb24c', 'f03b20'], + 4: ['ffffb2', 'fecc5c', 'fd8d3c', 'e31a1c'], + 5: ['ffffb2', 'fecc5c', 'fd8d3c', 'f03b20', 'bd0026'], + 6: ['ffffb2', 'fed976', 'feb24c', 'fd8d3c', 'f03b20', 'bd0026'], + 7: ['ffffb2', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', 'e31a1c', + 'b10026'], + 8: ['ffffcc', 'ffeda0', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', + 'e31a1c', 'b10026'], + 9: ['ffffcc', 'ffeda0', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', + 'e31a1c', 'bd0026', '800026'] + }, + YlOrBr: { + type: 'sequential', + cbf: 42, + 3: ['fff7bc', 'fec44f', 'd95f0e'], + 4: ['ffffd4', 'fed98e', 'fe9929', 'cc4c02'], + 5: ['ffffd4', 'fed98e', 'fe9929', 'd95f0e', '993404'], + 6: ['ffffd4', 'fee391', 'fec44f', 'fe9929', 'd95f0e', '993404'], + 7: ['ffffd4', 'fee391', 'fec44f', 'fe9929', 'ec7014', 'cc4c02', + '8c2d04'], + 8: ['ffffe5', 'fff7bc', 'fee391', 'fec44f', 'fe9929', 'ec7014', + 'cc4c02', '8c2d04'], + 9: ['ffffe5', 'fff7bc', 'fee391', 'fec44f', 'fe9929', 'ec7014', + 'cc4c02', '993404', '662506'] + }, + Purples: { + type: 'sequential', + cbf: 42, + 3: ['efedf5', 'bcbddc', '756bb1'], + 4: ['f2f0f7', 'cbc9e2', '9e9ac8', '6a51a3'], + 5: ['f2f0f7', 'cbc9e2', '9e9ac8', '756bb1', '54278f'], + 6: ['f2f0f7', 'dadaeb', 'bcbddc', '9e9ac8', '756bb1', '54278f'], + 7: ['f2f0f7', 'dadaeb', 'bcbddc', '9e9ac8', '807dba', '6a51a3', + '4a1486'], + 8: ['fcfbfd', 'efedf5', 'dadaeb', 'bcbddc', '9e9ac8', '807dba', + '6a51a3', '4a1486'], + 9: ['fcfbfd', 'efedf5', 'dadaeb', 'bcbddc', '9e9ac8', '807dba', + '6a51a3', '54278f', '3f007d'] + }, + Blues: { + type: 'sequential', + cbf: 42, + 3: ['deebf7', '9ecae1', '3182bd'], + 4: ['eff3ff', 'bdd7e7', '6baed6', '2171b5'], + 5: ['eff3ff', 'bdd7e7', '6baed6', '3182bd', '08519c'], + 6: ['eff3ff', 'c6dbef', '9ecae1', '6baed6', '3182bd', '08519c'], + 7: ['eff3ff', 'c6dbef', '9ecae1', '6baed6', '4292c6', '2171b5', + '084594'], + 8: ['f7fbff', 'deebf7', 'c6dbef', '9ecae1', '6baed6', '4292c6', + '2171b5', '084594'], + 9: ['f7fbff', 'deebf7', 'c6dbef', '9ecae1', '6baed6', '4292c6', + '2171b5', '08519c', '08306b'] + }, + Greens: { + type: 'sequential', + cbf: 42, + 3: ['e5f5e0', 'a1d99b', '31a354'], + 4: ['edf8e9', 'bae4b3', '74c476', '238b45'], + 5: ['edf8e9', 'bae4b3', '74c476', '31a354', '006d2c'], + 6: ['edf8e9', 'c7e9c0', 'a1d99b', '74c476', '31a354', '006d2c'], + 7: ['edf8e9', 'c7e9c0', 'a1d99b', '74c476', '41ab5d', '238b45', + '005a32'], + 8: ['f7fcf5', 'e5f5e0', 'c7e9c0', 'a1d99b', '74c476', '41ab5d', + '238b45', '005a32'], + 9: ['f7fcf5', 'e5f5e0', 'c7e9c0', 'a1d99b', '74c476', '41ab5d', + '238b45', '006d2c', '00441b'] + }, + Oranges: { + type: 'sequential', + cbf: 42, + 3: ['fee6ce', 'fdae6b', 'e6550d'], + 4: ['feedde', 'fdbe85', 'fd8d3c', 'd94701'], + 5: ['feedde', 'fdbe85', 'fd8d3c', 'e6550d', 'a63603'], + 6: ['feedde', 'fdd0a2', 'fdae6b', 'fd8d3c', 'e6550d', 'a63603'], + 7: ['feedde', 'fdd0a2', 'fdae6b', 'fd8d3c', 'f16913', 'd94801', + '8c2d04'], + 8: ['fff5eb', 'fee6ce', 'fdd0a2', 'fdae6b', 'fd8d3c', 'f16913', + 'd94801', '8c2d04'], + 9: ['fff5eb', 'fee6ce', 'fdd0a2', 'fdae6b', 'fd8d3c', 'f16913', + 'd94801', 'a63603', '7f2704'] + }, + Reds: { + type: 'sequential', + cbf: 42, + 3: ['fee0d2', 'fc9272', 'de2d26'], + 4: ['fee5d9', 'fcae91', 'fb6a4a', 'cb181d'], + 5: ['fee5d9', 'fcae91', 'fb6a4a', 'de2d26', 'a50f15'], + 6: ['fee5d9', 'fcbba1', 'fc9272', 'fb6a4a', 'de2d26', 'a50f15'], + 7: ['fee5d9', 'fcbba1', 'fc9272', 'fb6a4a', 'ef3b2c', 'cb181d', + '99000d'], + 8: ['fff5f0', 'fee0d2', 'fcbba1', 'fc9272', 'fb6a4a', 'ef3b2c', + 'cb181d', '99000d'], + 9: ['fff5f0', 'fee0d2', 'fcbba1', 'fc9272', 'fb6a4a', 'ef3b2c', + 'cb181d', 'a50f15', '67000d'] + }, + Greys: { + type: 'sequential', + cbf: 42, + 3: ['f0f0f0', 'bdbdbd', '636363'], + 4: ['f7f7f7', 'cccccc', '969696', '525252'], + 5: ['f7f7f7', 'cccccc', '969696', '636363', '252525'], + 6: ['f7f7f7', 'd9d9d9', 'bdbdbd', '969696', '636363', '252525'], + 7: ['f7f7f7', 'd9d9d9', 'bdbdbd', '969696', '737373', '525252', + '252525'], + 8: ['ffffff', 'f0f0f0', 'd9d9d9', 'bdbdbd', '969696', '737373', + '525252', '252525'], + 9: ['ffffff', 'f0f0f0', 'd9d9d9', 'bdbdbd', '969696', '737373', + '525252', '252525', '000000'] + }, + PuOr: { + type: 'diverging', + cbf: 42, + 3: ['f1a340', 'f7f7f7', '998ec3'], + 4: ['e66101', 'fdb863', 'b2abd2', '5e3c99'], + 5: ['e66101', 'fdb863', 'f7f7f7', 'b2abd2', '5e3c99'], + 6: ['b35806', 'f1a340', 'fee0b6', 'd8daeb', '998ec3', '542788'], + 7: ['b35806', 'f1a340', 'fee0b6', 'f7f7f7', 'd8daeb', '998ec3', + '542788'], + 8: ['b35806', 'e08214', 'fdb863', 'fee0b6', 'd8daeb', 'b2abd2', + '8073ac', '542788'], + 9: ['b35806', 'e08214', 'fdb863', 'fee0b6', 'f7f7f7', 'd8daeb', + 'b2abd2', '8073ac', '542788'], + 10: ['7f3b08', 'b35806', 'e08214', 'fdb863', 'fee0b6', 'd8daeb', + 'b2abd2', '8073ac', '542788', '2d004b'], + 11: ['7f3b08', 'b35806', 'e08214', 'fdb863', 'fee0b6', 'f7f7f7', + 'd8daeb', 'b2abd2', '8073ac', '542788', '2d004b'] + }, + BrBG: { + type: 'diverging', + cbf: 42, + 3: ['d8b365', 'f5f5f5', '5ab4ac'], + 4: ['a6611a', 'dfc27d', '80cdc1', '018571'], + 5: ['a6611a', 'dfc27d', 'f5f5f5', '80cdc1', '018571'], + 6: ['8c510a', 'd8b365', 'f6e8c3', 'c7eae5', '5ab4ac', '01665e'], + 7: ['8c510a', 'd8b365', 'f6e8c3', 'f5f5f5', 'c7eae5', '5ab4ac', + '01665e'], + 8: ['8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'c7eae5', '80cdc1', + '35978f', '01665e'], + 9: ['8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'f5f5f5', 'c7eae5', + '80cdc1', '35978f', '01665e'], + 10: ['543005', '8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'c7eae5', + '80cdc1', '35978f', '01665e', '003c30'], + 11: ['543005', '8c510a', 'bf812d', 'dfc27d', 'f6e8c3', 'f5f5f5', + 'c7eae5', '80cdc1', '35978f', '01665e', '003c30'] + }, + PRGn: { + type: 'diverging', + cbf: 42, + 3: ['af8dc3', 'f7f7f7', '7fbf7b'], + 4: ['7b3294', 'c2a5cf', 'a6dba0', '008837'], + 5: ['7b3294', 'c2a5cf', 'f7f7f7', 'a6dba0', '008837'], + 6: ['762a83', 'af8dc3', 'e7d4e8', 'd9f0d3', '7fbf7b', '1b7837'], + 7: ['762a83', 'af8dc3', 'e7d4e8', 'f7f7f7', 'd9f0d3', '7fbf7b', + '1b7837'], + 8: ['762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'd9f0d3', 'a6dba0', + '5aae61', '1b7837'], + 9: ['762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'f7f7f7', 'd9f0d3', + 'a6dba0', '5aae61', '1b7837'], + 10: ['40004b', '762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'd9f0d3', + 'a6dba0', '5aae61', '1b7837', '00441b'], + 11: ['40004b', '762a83', '9970ab', 'c2a5cf', 'e7d4e8', 'f7f7f7', + 'd9f0d3', 'a6dba0', '5aae61', '1b7837', '00441b'] + }, + PiYG: { + type: 'diverging', + cbf: 42, + 3: ['e9a3c9', 'f7f7f7', 'a1d76a'], + 4: ['d01c8b', 'f1b6da', 'b8e186', '4dac26'], + 5: ['d01c8b', 'f1b6da', 'f7f7f7', 'b8e186', '4dac26'], + 6: ['c51b7d', 'e9a3c9', 'fde0ef', 'e6f5d0', 'a1d76a', '4d9221'], + 7: ['c51b7d', 'e9a3c9', 'fde0ef', 'f7f7f7', 'e6f5d0', 'a1d76a', + '4d9221'], + 8: ['c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'e6f5d0', 'b8e186', + '7fbc41', '4d9221'], + 9: ['c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'f7f7f7', 'e6f5d0', + 'b8e186', '7fbc41', '4d9221'], + 10: ['8e0152', 'c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'e6f5d0', + 'b8e186', '7fbc41', '4d9221', '276419'], + 11: ['8e0152', 'c51b7d', 'de77ae', 'f1b6da', 'fde0ef', 'f7f7f7', + 'e6f5d0', 'b8e186', '7fbc41', '4d9221', '276419'] + }, + RdBu: { + type: 'diverging', + cbf: 42, + 3: ['ef8a62', 'f7f7f7', '67a9cf'], + 4: ['ca0020', 'f4a582', '92c5de', '0571b0'], + 5: ['ca0020', 'f4a582', 'f7f7f7', '92c5de', '0571b0'], + 6: ['b2182b', 'ef8a62', 'fddbc7', 'd1e5f0', '67a9cf', '2166ac'], + 7: ['b2182b', 'ef8a62', 'fddbc7', 'f7f7f7', 'd1e5f0', '67a9cf', + '2166ac'], + 8: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'd1e5f0', '92c5de', + '4393c3', '2166ac'], + 9: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'f7f7f7', 'd1e5f0', + '92c5de', '4393c3', '2166ac'], + 10: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'd1e5f0', + '92c5de', '4393c3', '2166ac', '053061'], + 11: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'f7f7f7', + 'd1e5f0', '92c5de', '4393c3', '2166ac', '053061'] + }, + RdGy: { + type: 'diverging', + cbf: 42, + 3: ['ef8a62', 'ffffff', '999999'], + 4: ['ca0020', 'f4a582', 'bababa', '404040'], + 5: ['ca0020', 'f4a582', 'ffffff', 'bababa', '404040'], + 6: ['b2182b', 'ef8a62', 'fddbc7', 'e0e0e0', '999999', '4d4d4d'], + 7: ['b2182b', 'ef8a62', 'fddbc7', 'ffffff', 'e0e0e0', '999999', + '4d4d4d'], + 8: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'e0e0e0', 'bababa', + '878787', '4d4d4d'], + 9: ['b2182b', 'd6604d', 'f4a582', 'fddbc7', 'ffffff', 'e0e0e0', + 'bababa', '878787', '4d4d4d'], + 10: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'e0e0e0', + 'bababa', '878787', '4d4d4d', '1a1a1a'], + 11: ['67001f', 'b2182b', 'd6604d', 'f4a582', 'fddbc7', 'ffffff', + 'e0e0e0', 'bababa', '878787', '4d4d4d', '1a1a1a'] + }, + RdYlBu: { + type: 'diverging', + cbf: 42, + 3: ['fc8d59', 'ffffbf', '91bfdb'], + 4: ['d7191c', 'fdae61', 'abd9e9', '2c7bb6'], + 5: ['d7191c', 'fdae61', 'ffffbf', 'abd9e9', '2c7bb6'], + 6: ['d73027', 'fc8d59', 'fee090', 'e0f3f8', '91bfdb', '4575b4'], + 7: ['d73027', 'fc8d59', 'fee090', 'ffffbf', 'e0f3f8', '91bfdb', + '4575b4'], + 8: ['d73027', 'f46d43', 'fdae61', 'fee090', 'e0f3f8', 'abd9e9', + '74add1', '4575b4'], + 9: ['d73027', 'f46d43', 'fdae61', 'fee090', 'ffffbf', 'e0f3f8', + 'abd9e9', '74add1', '4575b4'], + 10: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee090', 'e0f3f8', + 'abd9e9', '74add1', '4575b4', '313695'], + 11: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee090', 'ffffbf', + 'e0f3f8', 'abd9e9', '74add1', '4575b4', '313695'] + }, + Spectral: { + type: 'diverging', + cbf: 0, + 3: ['fc8d59', 'ffffbf', '99d594'], + 4: ['d7191c', 'fdae61', 'abdda4', '2b83ba'], + 5: ['d7191c', 'fdae61', 'ffffbf', 'abdda4', '2b83ba'], + 6: ['d53e4f', 'fc8d59', 'fee08b', 'e6f598', '99d594', '3288bd'], + 7: ['d53e4f', 'fc8d59', 'fee08b', 'ffffbf', 'e6f598', '99d594', + '3288bd'], + 8: ['d53e4f', 'f46d43', 'fdae61', 'fee08b', 'e6f598', 'abdda4', + '66c2a5', '3288bd'], + 9: ['d53e4f', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', 'e6f598', + 'abdda4', '66c2a5', '3288bd'], + 10: ['9e0142', 'd53e4f', 'f46d43', 'fdae61', 'fee08b', 'e6f598', + 'abdda4', '66c2a5', '3288bd', '5e4fa2'], + 11: ['9e0142', 'd53e4f', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', + 'e6f598', 'abdda4', '66c2a5', '3288bd', '5e4fa2'] + }, + RdYlGn: { + type: 'diverging', + cbf: 0, + 3: ['fc8d59', 'ffffbf', '91cf60'], + 4: ['d7191c', 'fdae61', 'a6d96a', '1a9641'], + 5: ['d7191c', 'fdae61', 'ffffbf', 'a6d96a', '1a9641'], + 6: ['d73027', 'fc8d59', 'fee08b', 'd9ef8b', '91cf60', '1a9850'], + 7: ['d73027', 'fc8d59', 'fee08b', 'ffffbf', 'd9ef8b', '91cf60', + '1a9850'], + 8: ['d73027', 'f46d43', 'fdae61', 'fee08b', 'd9ef8b', 'a6d96a', + '66bd63', '1a9850'], + 9: ['d73027', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', 'd9ef8b', + 'a6d96a', '66bd63', '1a9850'], + 10: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee08b', 'd9ef8b', + 'a6d96a', '66bd63', '1a9850', '006837'], + 11: ['a50026', 'd73027', 'f46d43', 'fdae61', 'fee08b', 'ffffbf', + 'd9ef8b', 'a6d96a', '66bd63', '1a9850', '006837'] + }, + Accent: { + type: 'qualitative', + cbf: 0, + 3: ['7fc97f', 'beaed4', 'fdc086'], + 4: ['7fc97f', 'beaed4', 'fdc086', 'ffff99'], + 5: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0'], + 6: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0', 'f0027f'], + 7: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0', 'f0027f', + 'bf5b17'], + 8: ['7fc97f', 'beaed4', 'fdc086', 'ffff99', '386cb0', 'f0027f', + 'bf5b17', '666666'] + }, + Dark2: { + type: 'qualitative', + cbf: 3, + 3: ['1b9e77', 'd95f02', '7570b3'], + 4: ['1b9e77', 'd95f02', '7570b3', 'e7298a'], + 5: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e'], + 6: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e', 'e6ab02'], + 7: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e', 'e6ab02', + 'a6761d'], + 8: ['1b9e77', 'd95f02', '7570b3', 'e7298a', '66a61e', 'e6ab02', + 'a6761d', '666666'] + }, + Paired: { + type: 'qualitative', + cbf: 4, + 3: ['a6cee3', '1f78b4', 'b2df8a'], + 4: ['a6cee3', '1f78b4', 'b2df8a', '33a02c'], + 5: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99'], + 6: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c'], + 7: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', + 'fdbf6f'], + 8: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', + 'fdbf6f', 'ff7f00'], + 9: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', + 'fdbf6f', 'ff7f00', 'cab2d6'], + 10: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', + 'fdbf6f', 'ff7f00', 'cab2d6', '6a3d9a'], + 11: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', + 'fdbf6f', 'ff7f00', 'cab2d6', '6a3d9a', 'ffff99'], + 12: ['a6cee3', '1f78b4', 'b2df8a', '33a02c', 'fb9a99', 'e31a1c', + 'fdbf6f', 'ff7f00', 'cab2d6', '6a3d9a', 'ffff99', 'b15928'] + }, + Pastel1: { + type: 'qualitative', + cbf: 0, + 3: ['fbb4ae', 'b3cde3', 'ccebc5'], + 4: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4'], + 5: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6'], + 6: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc'], + 7: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc', + 'e5d8bd'], + 8: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc', + 'e5d8bd', 'fddaec'], + 9: ['fbb4ae', 'b3cde3', 'ccebc5', 'decbe4', 'fed9a6', 'ffffcc', + 'e5d8bd', 'fddaec', 'f2f2f2'] + }, + Pastel2: { + type: 'qualitative', + cbf: 0, + 3: ['b3e2cd', 'fdcdac', 'cbd5e8'], + 4: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4'], + 5: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9'], + 6: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9', 'fff2ae'], + 7: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9', 'fff2ae', + 'f1e2cc'], + 8: ['b3e2cd', 'fdcdac', 'cbd5e8', 'f4cae4', 'e6f5c9', 'fff2ae', + 'f1e2cc', 'cccccc'] + }, + Set1: { + type: 'qualitative', + cbf: 0, + 3: ['e41a1c', '377eb8', '4daf4a'], + 4: ['e41a1c', '377eb8', '4daf4a', '984ea3'], + 5: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00'], + 6: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33'], + 7: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33', + 'a65628'], + 8: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33', + 'a65628', 'f781bf'], + 9: ['e41a1c', '377eb8', '4daf4a', '984ea3', 'ff7f00', 'ffff33', + 'a65628', 'f781bf', '999999'] + }, + Set2: { + type: 'qualitative', + cbf: 3, + 3: ['66c2a5', 'fc8d62', '8da0cb'], + 4: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3'], + 5: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854'], + 6: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854', 'ffd92f'], + 7: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854', 'ffd92f', + 'e5c494'], + 8: ['66c2a5', 'fc8d62', '8da0cb', 'e78ac3', 'a6d854', 'ffd92f', + 'e5c494', 'b3b3b3'] + }, + Set3: { + type: 'qualitative', + cbf: 0, + 3: ['8dd3c7', 'ffffb3', 'bebada'], + 4: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072'], + 5: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3'], + 6: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462'], + 7: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', + 'b3de69'], + 8: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', + 'b3de69', 'fccde5'], + 9: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', + 'b3de69', 'fccde5', 'd9d9d9'], + 10: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', + 'b3de69', 'fccde5', 'd9d9d9', 'bc80bd'], + 11: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', + 'b3de69', 'fccde5', 'd9d9d9', 'bc80bd', 'ccebc5'], + 12: ['8dd3c7', 'ffffb3', 'bebada', 'fb8072', '80b1d3', 'fdb462', + 'b3de69', 'fccde5', 'd9d9d9', 'bc80bd', 'ccebc5', 'ffed6f'] + }, + Custom1: { + type: 'qualitative', + cbf: 0, + 1: ['3aa5ff'], + 2: ['42d4f4', 'f032e6'], + 3: ['f96a00', 'ffa866', 'ffd4b3'], + 5: ['ffcd22', '911db4', '373737','ff0058','469990'], + 6: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4'], + 7: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4'], + 8: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990'], + 9: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff'], + 10: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff','9a6324'], + 11: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff','9a6324','fffac8'], + 12: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff','9a6324','fffac8','800000'], + 18: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff','9a6324','fffac8','800000','aaffc3','808001', + 'bfef45','000075','fabebe','a9a9a9'], + 20: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff','9a6324','fffac8','800000','aaffc3','808001', + 'bfef45','000075','fabebe','a9a9a9','000000','f032e6','ffd7b1'], + 50: ['e6194b','3cb44b','ffcd22','4364d8','f58230','911db4', '42d4f4', + '469990','e6bdff','9a6324','fffac8','800000','aaffc3','808001', + 'bfef45','000075','fabebe','a9a9a9','000000','f032e6','ffd7b1'], + }, + BasePalette: { + type: 'qualitative', + cbf: 0, + 1: ['024BA3'], + 2: ['024BA3', 'E24577'], + 3: ['024BA3', 'E24577', 'F58834'], + 5: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC'], + 6: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4'], + 7: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B'], + 8: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770'], + 9: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94'], + 10: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94', 'E24577'], + 11: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94', 'E24577', 'F56155'], + 12: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94', 'E24577', 'F56155', 'F58834'], + 18: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834'], + 20: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4'], + 50: ['024BA3', '0068BE', '0082CB', '019AC9', '02B1BC', '00C6A4', '00D88B', '8AE770', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4'], + }, + BasePalette2: { + type: 'qualitative', + cbf: 0, + 1: ['F3457E'], + 2: ['024BA3', '7B47A4'], + 3: ['024BA3', '7B47A4', 'B93F94'], + 5: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155'], + 6: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834'], + 7: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3'], + 8: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4'], + 9: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94'], + 10: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577'], + 11: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155'], + 12: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834'], + 18: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834'], + 20: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4'], + 50: ['024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4', 'B93F94', 'E24577', 'F56155', 'F58834', '024BA3', '7B47A4'], + }, + }; + + for (var name in schemes) { + var scheme = schemes[name]; + scheme = palette.Scheme.fromPalettes( + 'cb-' + name, [scheme.type, 'cb-' + scheme.type], scheme, 12, scheme.cbf); + palette.register(scheme); + } + })(); + + if(typeof module === "object" && module.exports) { + module.exports = palette + } + \ No newline at end of file diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 73a78d54a0ccdf8a82bd8872a66dae4dac236e30..eafe618723ba6bf1aa631679ade9742b9c93b59b 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -142,6 +142,14 @@ class Header extends Component<LoginProps, LoginState> { }`}>ALL APPLICATIONS</Link> </li> )} + {Helper.getUserRole() === APP.ROLE.REGULATOR && ( + <li className="mr-5"> + <Link to={"/analytics"} className={`${this.props.history.location.pathname.match("/analytics") + ? "active" + : "" + }`}>DASHBOARD</Link> + </li> + )} </ul> } { diff --git a/src/constants/ApiConstants.ts b/src/constants/ApiConstants.ts index 0dfc07744aaeca45db4d66558cca97fefd2dbb91..80e50ca6883a1d5b51a3fa66206d8a4566ee043b 100644 --- a/src/constants/ApiConstants.ts +++ b/src/constants/ApiConstants.ts @@ -33,4 +33,9 @@ export const APIS = { GET_USER_BY_ID: "user/getUserById", GET_ALL_USERS: "user/v1/getAllUser", }, + DASHBOARD: { + GET_DASHBOARD_CONFIG: "dashboard/getDashboardConfig/SMF/home", + GET_DASHBOARD_PROFILE: "dashboard/getDashboardsForProfile/SMF", + GET_CHART_DATA: "dashboard/getChartV2/SMF", + }, }; diff --git a/src/helpers/exportChart.js b/src/helpers/exportChart.js new file mode 100644 index 0000000000000000000000000000000000000000..b6f9c21dbd31af39e77cb2ad71e4a6ed2b17a452 --- /dev/null +++ b/src/helpers/exportChart.js @@ -0,0 +1,103 @@ +import NFormatterFun from "../components/charts/NFormatterFun"; +const ExportChart = { + _charts: {}, + + setAttribute(key, value) { + this._charts[key] = value; + }, + + getRows(chartCode) { + var rows = []; + var chartData = this._charts[chartCode][0].plots; + var tempVal; + chartData.forEach((d1, i) => { + if (i === 0) { + if (d1.parentLabel !== null && d1.parentLabel !== "") { + rows.push([d1.label, d1.valueLabel, d1.parentLabel]); + } else { + rows.push([d1.label, d1.valueLabel]); + } + } + tempVal = NFormatterFun(d1.value, d1.symbol, "Unit"); + tempVal = + typeof tempVal == "string" + ? parseFloat(tempVal.replace(/,/g, "")) + : tempVal; + if (d1.parentName !== null && d1.parentName !== "") { + rows.push([d1.name, tempVal, d1.parentName]); + } else { + rows.push([d1.name, tempVal]); + } + }); + + return rows; + }, + + tableToCsv(filename) { + var tableParentId = "#" + filename.replace(/\s/g, ""); + var csv = []; + var rows = document.querySelectorAll(tableParentId + " table tr"); + if (rows.length === 0 && rows !== undefined) { + rows = document.querySelectorAll("#modalView table tr"); + } + for (var i = 0; i < rows.length; i++) { + var row = [], + cols = rows[i].querySelectorAll("td, th"); + + for (var j = 0; j < cols.length; j++) + row.push(cols[j].innerText.replace(/,/g, "")); + + csv.push(row.join(",")); + } + + // Download CSV file + this.downloadCSV(csv.join("\n"), filename); + }, + + toCsv(filename, chartCode) { + var rows = this.getRows(chartCode); + var processRow = function (row) { + var finalVal = ""; + for (var j = 0; j < row.length; j++) { + var innerValue = row[j] === null ? "" : row[j].toString(); + if (row[j] instanceof Date) { + innerValue = row[j].toLocaleString(); + } + var result = innerValue.replace(/"/g, '""'); + if (result.search(/("|,|\n)/g) >= 0) result = '"' + result + '"'; + if (j > 0) finalVal += ","; + finalVal += result; + } + return finalVal + "\n"; + }; + + var csvFile = ""; + for (var i = 0; i < rows.length; i++) { + csvFile += processRow(rows[i]); + } + this.downloadCSV(csvFile, filename); + }, + + downloadCSV(csvFile, filename) { + var blob = new Blob([csvFile], { type: "text/csv;charset=utf-8;" }); + if (navigator.msSaveBlob) { + // IE 10+ + navigator.msSaveBlob(blob, filename); + } else { + var link = document.createElement("a"); + if (link.download !== undefined) { + // feature detection + // Browsers that support HTML5 download attribute + var url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", filename + ".csv"); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } + }, +}; + +export default ExportChart; diff --git a/src/index.tsx b/src/index.tsx index bdacd3935c16656dcde08f021127974067fdc332..3b91690a14ec1f22f440c1fd7b64490c4ec2c8f4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,5 @@ import 'bootstrap/dist/css/bootstrap.css'; +import 'bootstrap/dist/js/bootstrap.js'; import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; diff --git a/src/layouts/Dashboard/DashboardLayout.tsx b/src/layouts/Dashboard/DashboardLayout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cbefac85a582acd5b947a0413a2eaaf42947630d --- /dev/null +++ b/src/layouts/Dashboard/DashboardLayout.tsx @@ -0,0 +1,56 @@ +/*eslint-disable no-empty-pattern*/ +// @ts-nocheck +import _ from "lodash"; +import PageLayout from "./PageLayout"; +import { useHistory } from "react-router-dom"; +import WidgetNavBar from "../../components/charts/WidgetNavBar"; + +/** + * DashboardLayout component renders + * the visualisations based on the configurations + * for regulator + */ + +interface DashboardLayoutProps { + dashboardConfig: any; +} + +export const DashboardLayout = ({ dashboardConfig }: DashboardLayoutProps) => { + let history = useHistory(); + + const renderCharts = () => { + let dashboardConfigData: any = dashboardConfig; + + let tabsInitData = _.chain(dashboardConfigData) + .first() + .get("visualizations") + .groupBy("name") + .value(); + + return ( + <div> + {_.map(tabsInitData, (k, v) => { + return ( + <PageLayout + key={v} + chartRowData={k} + row={k.row} + pathName={history.location.pathname} + /> + ); + })} + </div> + ); + }; + + return ( + <div className=""> + <div className=""> + <WidgetNavBar history={history}/> + </div> + <div className="row pt-2"> + {dashboardConfig.length > 0 && renderCharts()} + </div> + </div> + ); +}; diff --git a/src/layouts/Dashboard/PageLayout.js b/src/layouts/Dashboard/PageLayout.js new file mode 100644 index 0000000000000000000000000000000000000000..0ece9fd561a9f3b6256882bf3a2491349db7aca1 --- /dev/null +++ b/src/layouts/Dashboard/PageLayout.js @@ -0,0 +1,34 @@ +import React, { Component } from "react"; +import GenericCharts from "../../components/charts/GenericCharts"; + +/** + * Page layout to display the charts which are generated dynamically + */ + +class PageLayout extends Component { + constructor(props) { + super(props); + this.state = { + data: null, + }; + } + + render() { + let { chartRowData, row } = this.props; + + return ( + <div key={`generic${row}`}> + {chartRowData.map((vizData, j) => ( + <GenericCharts + key={j} + row={row} + chartData={vizData} + pathProps={this.props.pathName} + /> + ))} + </div> + ); + } +} + +export default PageLayout; diff --git a/src/layouts/Inspector/FormView.tsx b/src/layouts/Inspector/FormView.tsx index 38cedb83317132e043e2015fcfc41abd4a393d97..6afd451a602ffff63555806be2bdebd7bde776ff 100644 --- a/src/layouts/Inspector/FormView.tsx +++ b/src/layouts/Inspector/FormView.tsx @@ -291,8 +291,10 @@ export const FormView = ({ applicationData, formData }: FormViewProps) => { setModalInspectionValue(m.inspectionValue); if (status === "correct") { m.isCorrect = true; - m.comments = m.comments; - m.inspectionValue = m.inspectionValue; + m.comments = ""; + m.inspectionValue = ""; + setModalTextArea(""); + setModalInspectionValue(""); } else if (status === "incorrect") { m.isCorrect = false; m.comments = m.comments; diff --git a/src/layouts/index.ts b/src/layouts/index.ts index 113d4eda6eba35037f11172716b5d895fe22d6ea..bf2607aa8173cc3de95b2fb521cb90fe4355d84e 100644 --- a/src/layouts/index.ts +++ b/src/layouts/index.ts @@ -6,3 +6,4 @@ export { ReviewApplicationLayout } from "./Regulator/ReviewApplicationLayout"; export { InspectionSummaryLayout } from "./Inspector/InspectionSummaryLayout"; export { InspectionCompleteLayout } from "./Inspector/InspectionCompleteLayout"; export { ConsentFormView } from "./Inspector/ConsentFormView"; +export { DashboardLayout } from "./Dashboard/DashboardLayout"; diff --git a/src/pages/Dashboard/Landing.tsx b/src/pages/Dashboard/Landing.tsx new file mode 100644 index 0000000000000000000000000000000000000000..66fb27f591e654a3922020c7a4e1085e2e99e821 --- /dev/null +++ b/src/pages/Dashboard/Landing.tsx @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Fragment, useEffect, useState } from "react"; +import { HeadingOne } from "../../components/headings"; +import Header from "../../components/common/Header"; +import { useHistory } from "react-router-dom"; +import { DashboardLayout } from "../../layouts"; +import { ChartService } from "../../services"; +import { APP } from "../../constants"; +import Notify from "../../helpers/notify"; + +/** + * Landing component renders + * dashboard layout and its UI components + */ + +interface LandingProps { + data?: any; +} + +export const Landing = ({ data }: LandingProps) => { + let history = useHistory(); + + const [dashboardConfigData, setDashboardConfigData] = useState<any[]>([]); + const [trigger, setTrigger] = useState(false); + const [toggle, setToggle] = useState(false); + const [showFilters, setShowFilters] = useState(false); + + useEffect(() => { + getDashboardConfigurations(); + }, []); + + const getDashboardConfigurations = () => { + ChartService.getDashboardConfig().then((response) => { + if (response.statusInfo.statusCode === APP.CODE.SUCCESS) { + setDashboardConfigData(response.responseData) + } else { + Notify.error(response.statusInfo.errorMessage); + } + }); + }; + + return ( + <Fragment> + <Header history={history} /> + <div className="container-fluid"> + <div className="container dashboard-inner-container mt-4"> + {/* Section one */} + <section className="pt-3"> + <HeadingOne heading="Insights so far" /> + </section> + + {/* Dashboards */} + <section className=""> + <DashboardLayout dashboardConfig={dashboardConfigData}/> + </section> + </div> + </div> + </Fragment> + ); +}; diff --git a/src/pages/Inspector/InspectorApplications.tsx b/src/pages/Inspector/InspectorApplications.tsx index 3bc76a0d291628ac290da457f23fd7d3e06c35d4..9445c927f51dd9a866343058dd1a1996dfe741de 100644 --- a/src/pages/Inspector/InspectorApplications.tsx +++ b/src/pages/Inspector/InspectorApplications.tsx @@ -151,6 +151,7 @@ export const InspectorApplications = ({ data }: InspectorApplicationsProps) => { userDetails && userDetails.id ) ) { + // consol i.inspection.assignedTo.map((m: any, n: number) => { if (m.id === userDetails.id) { // if (m.status === LANG.FORM_STATUS.INSPECTION_COMPLETED) { diff --git a/src/pages/index.ts b/src/pages/index.ts index 038393ffa67aa325558c449fbe42b8850af26258..fd7753a899f221f49ac3c275d7faccd2df9b86ce 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -6,3 +6,4 @@ export { ReviewApplication } from "./Regulator/ReviewApplication"; export { InspectionSummary } from "./Inspector/InspectionSummary"; export { InspectionComplete } from "./Inspector/InspectionComplete"; export { ViewConsentApplications } from "./Inspector/ViewConsentApplication"; +export { Landing } from "./Dashboard/Landing"; diff --git a/src/services/chart.service.js b/src/services/chart.service.js new file mode 100644 index 0000000000000000000000000000000000000000..7200b96b33a657534abab7db7657cbc739d7b8f8 --- /dev/null +++ b/src/services/chart.service.js @@ -0,0 +1,75 @@ +import { APIS, APP, LANG } from "../constants"; +import { authHeader } from "../helpers/authHeader"; +import Notify from "./../helpers/notify"; +import { UserService } from "./user.service"; + +/** + * Chart service + * Provides API functions, returns + * data required for the charts + */ + +export const ChartService = { + getDashboardConfig, + getDashboardProfile, + getChartData, +}; + +async function getDashboardConfig() { + const requestOptions = { + method: APP.REQUEST.GET, + headers: authHeader(), + }; + return await fetch( + APIS.BASE_URL + APIS.DASHBOARD.GET_DASHBOARD_CONFIG, + requestOptions + ).then(handleResponse); +} + +async function getDashboardProfile() { + const requestOptions = { + method: APP.REQUEST.GET, + headers: authHeader(), + }; + return await fetch( + APIS.BASE_URL + APIS.DASHBOARD.GET_DASHBOARD_PROFILE, + requestOptions + ).then(handleResponse); +} + +async function getChartData(payload) { + const requestOptions = { + method: APP.REQUEST.POST, + body: JSON.stringify(payload), + headers: authHeader(), + }; + return await fetch( + APIS.BASE_URL + APIS.DASHBOARD.GET_CHART_DATA, + requestOptions + ).then(handleResponse); +} + +function handleResponse(response) { + return response.text().then((text) => { + const data = text && JSON.parse(text); + if (!response.ok) { + const error = + LANG.APIERROR || + (data && data.statusInfo && data.statusInfo.errorMessage) || + response.statusText; + return Promise.reject(new Error(error)); + } + if (data && data.statusInfo && data.statusInfo.statusCode) { + if (data.statusInfo.statusCode === 306) { + const error = + (data && data.statusInfo && data.statusInfo.errorMessage) || + response.statusText; + UserService.logout(); + Notify.error(error.message); + window.location.reload(); + return Promise.reject(new Error(error)); + } + } + return data; + }); +} diff --git a/src/services/index.ts b/src/services/index.ts index c9237033708069aa301f56d9c545488a1182ec2b..53c49458eed0f020f8790ed0225bfa1e22d078f4 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,3 +1,4 @@ export { ReviewService } from "./review.service"; export { FormService } from "./form.service"; export { UserService } from "./user.service"; +export { ChartService } from "./chart.service"; diff --git a/src/styles/chart.css b/src/styles/chart.css new file mode 100644 index 0000000000000000000000000000000000000000..269f0066cab901b66ed19406db039084a9454ffc --- /dev/null +++ b/src/styles/chart.css @@ -0,0 +1,57 @@ +@import url("./colors.css"); +@import url("../assets/fonts/fonts.css"); + +.chart_title_one { + color: var(--black-87); + font-family: "Montserrat-SemiBold"; + font-size: 1rem; + letter-spacing: 0.12px; + line-height: 1.5; +} + +.chart_card_one { + background-color: var(--white-100); + border: 1px solid var(--black-08); + border-radius: 4px; + padding: 1.75rem; +} + +.widget_card_one { + background-color: var(--white-100); + border-radius: 4px; + border: 1px solid var(--black-08); + min-height: 5.563rem; +} + +.widget_card_one label { + color: var(--black-87); + font-family: "Lato-Regular"; + font-size: 0.875rem; + letter-spacing: 0.25px; + line-height: 1.5; +} + +.widget_card_one h2 { + color: var(--black-87); + font-family: "Montserrat-SemiBold"; + font-size: 1.25rem; + letter-spacing: 0.12px; + line-height: 1.4; +} + +.custom_cursor { + cursor: pointer; +} + +.dropdown_text { + font-family: "Lato-Regular"; + font-size: 0.875rem; +} + +.chart_card_description_one { + color: var(--black-60); + font-family:"Lato-Regular"; + font-size: 0.875rem; + letter-spacing: 0.25; + line-height: 1.429; +} diff --git a/yarn.lock b/yarn.lock index 12a63aa67e1d6a7eb61d1fd968db17787c76c378..f97eb311c7636315f113951dd6e21cc536029b72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1315,6 +1315,11 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@popperjs/core@^2.11.4": + version "2.11.4" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503" + integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== + "@rollup/plugin-babel@^5.2.0": version "5.3.0" resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz" @@ -1720,6 +1725,11 @@ resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/lodash@^4.14.118": + version "4.14.180" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" + integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== + "@types/mime@^1": version "1.3.2" resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz" @@ -2782,6 +2792,16 @@ char-regex@^2.0.0: resolved "https://registry.npmjs.org/char-regex/-/char-regex-2.0.0.tgz" integrity sha512-oGu2QekBMXgyQNWPDRQ001bjvDnZe4/zBTz37TMbiKz1NbNiyiH5hRkobe7npRN6GfbGbxMYFck/vQ1r9c1VMA== +chart.js@^3.4.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.1.tgz#0516f690c6a8680c6c707e31a4c1807a6f400ada" + integrity sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA== + +chartjs-plugin-datalabels@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.0.0.tgz#caacefb26803d968785071eab012dde8746c5939" + integrity sha512-WBsWihphzM0Y8fmQVm89+iy99mmgejmj5/jcsYqwxSioLRL/zqJ4Scv/eXq5ZqvG3TpojlGzZLeaOaSvDm7fwA== + check-types@^11.1.1: version "11.1.2" resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz" @@ -3499,6 +3519,11 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-to-image@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867" + integrity sha1-ilA2CAiMh7HCL5A0rgMuGJiVWGc= + domelementtype@1: version "1.3.1" resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz" @@ -3992,6 +4017,11 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= + exit@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" @@ -4105,6 +4135,11 @@ file-loader@^6.2.0: loader-utils "^2.0.0" schema-utils "^3.0.0" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + filelist@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz" @@ -5694,12 +5729,12 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6987,6 +7022,11 @@ react-app-polyfill@^3.0.0: regenerator-runtime "^0.13.9" whatwg-fetch "^3.6.2" +react-chartjs-2@^3.0.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-3.3.0.tgz#3f62681645acccc1ab27a9186c70f79d6d417f73" + integrity sha512-4Mt0SR2aiUbWi/4762odRBYSnbNKSs4HWc0o3IW43py5bMfmfpeZU95w6mbvtuLZH/M3GsPJMU8DvDc+5U9blQ== + react-confirm-alert@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/react-confirm-alert/-/react-confirm-alert-2.7.0.tgz#7cdbf2e052e03554ac6a7d8956c9c227010c8a53" @@ -7046,6 +7086,11 @@ react-is@^17.0.1: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + react-loadable@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/react-loadable/-/react-loadable-5.5.0.tgz" @@ -7060,6 +7105,16 @@ react-localization@^1.0.17: dependencies: localized-strings "^0.2.0" +react-modal@^3.14.4: + version "3.14.4" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.14.4.tgz#2ca7e8e9a180955e5c9508c228b73167c1e6f6a3" + integrity sha512-8surmulejafYCH9wfUmFyj4UfbSJwjcgbS9gf3oOItu4Hwd6ivJyVBETI0yHRhpJKCLZMUtnhzk76wXTsNL6Qg== + dependencies: + exenv "^1.2.0" + prop-types "^15.7.2" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" @@ -8412,6 +8467,13 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz"