import React from "react"; import { withStyles } from "@material-ui/core/styles"; import { t } from "../utils/t"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import Button from "@material-ui/core/Button"; import ButtonGroup from "@material-ui/core/ButtonGroup"; import CircularProgress from "@material-ui/core/CircularProgress"; import Fade from "@material-ui/core/Fade"; import Skeleton from "@material-ui/lab/Skeleton"; import ChartDot from "../icons/ChartDot"; import { scaleTime } from "d3-scale"; import {utcHour, utcDay, utcMonth, utcWeek, utcYear, utcMonday, utcFriday, utcSaturday, utcSunday} from "d3-time"; import { FIRST_WEEK_DAY_BY_COUNTRY } from "../utils/constants"; import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts"; import price_formatter from "../utils/price-formatter"; import api from "../utils/api"; import actions from "../actions/utils"; const styles = theme => ({ root: { flexGrow: 1, padding: theme.spacing(1), [theme.breakpoints.down("sm")]: { padding: theme.spacing(1, 0), width: "100vw" } }, chart: { width: "100%", height: 500, [theme.breakpoints.down("sm")]: { height: 250, } }, floatRight: { float: "right", marginBottom: theme.spacing(1) }, floatLeft: { float: "left", marginBottom: theme.spacing(1) }, fullHeight: { height: "100%", position: "relative" }, chartCardContent: { [theme.breakpoints.down("sm")]: { padding: theme.spacing(2, 0) } }, flowRoot: { display: "flow-root" }, contrastButton: { backgroundColor: theme.palette.primary.main, color: theme.palette.secondary.contrastText, "&:hover": { backgroundColor: theme.palette.primary.main, color: theme.palette.secondary.contrastText, } }, contrast: theme.palette.secondary.contrast, overlay: { position: "absolute", top: "50%", left: "50%", width: "100%", height: "100%", transform: "translate(-50%, -50%)", backgroundColor: "rgba(0, 0, 0, .0)", zIndex: 1, pointerEvents: "none", color: theme.palette.primary.light }, circularProgressContainer: { position: "absolute", top: "50%", left: "50%", width: "100%", transform: "translate(-50%, -50%)", textAlign: "center", } }); class CoinChartsChart extends React.Component { constructor(props) { super(props); this.state = { classes: props.classes, coin_id: props.coin_id, selected_currency: props.selected_currency, selected_locales_code: props.selected_locales_code, _coin_chart_data: null, _is_coin_chart_data_loading: false, _coin_chart_data_time: "360", _coin_chart_data_type: "prices", _regular_formatted_complete_sorted_data: [], _regular_complete_sorted_data: {}, _ticks_array: [], _coin_chart_data_loaded: false, _bitcoin_chart_data_loaded: false, }; }; componentWillReceiveProps(new_props) { const { coin_id } = this.state; this.setState(new_props, function(){ if(coin_id !== new_props.coin_id) { this._get_coin_chart_data(); } }); } componentDidMount() { this._get_coin_chart_data(); } _get_coin_chart_data() { const { coin_id, selected_currency, _coin_chart_data_time } = this.state; actions.trigger_loading_update(0); this.setState({_is_coin_chart_data_loading: true, _coin_chart_data_loaded: false, _bitcoin_chart_data_loaded: false}, () => { api.get_coin_chart_data(coin_id, selected_currency.toLowerCase(), _coin_chart_data_time, (error, response) => { this._set_coin_chart_data(error, response, "coin_id") }); api.get_coin_chart_data("bitcoin", selected_currency.toLowerCase(), _coin_chart_data_time, (error, response) => { this._set_coin_chart_data(error, response, "bitcoin") }); }); } _set_coin_chart_data = (error, _coin_chart_data, coin_id) => { if(!error && _coin_chart_data) { let { _regular_complete_sorted_data, _ticks_array } = this.state; let { _coin_chart_data_loaded, _bitcoin_chart_data_loaded } = this.state; _coin_chart_data_loaded = coin_id === "coin_id" ? true: _coin_chart_data_loaded; _bitcoin_chart_data_loaded = coin_id === "bitcoin" ? true: _bitcoin_chart_data_loaded; _regular_complete_sorted_data[coin_id] = []; let _is_coin_chart_data_loading = !_coin_chart_data_loaded || !_bitcoin_chart_data_loaded; this.setState({_is_coin_chart_data_loading, _coin_chart_data_loaded, _bitcoin_chart_data_loaded}, () => { const { _coin_chart_data_type, _coin_chart_data_time, _is_coin_chart_data_loading } = this.state; const graph_data = _coin_chart_data !== null ? _coin_chart_data[_coin_chart_data_type].map((row) => { row.date = parseFloat(row.date); return row; }): []; const sorted_graph_data = graph_data.sort(function(a, b) { return a.date - b.date; }); _ticks_array = this._get_coin_chart_data_ticks(sorted_graph_data, _coin_chart_data_time); const complete_data = this._get_graph_data_with_ticks(sorted_graph_data, _ticks_array); const sorted_complete_graph_data = complete_data.sort(function(a, b) { return a.date - b.date; }); sorted_complete_graph_data.forEach(function(item, index, array) { if(item.value === "irregular") { const previous_item = index > 0 ? array[index-1].value: array[index].value; const next_item = index < array.length-1 ? array[index+1].value: array[index].value; const middle_item = { value: (previous_item + next_item) / 2, date: item.date }; _regular_complete_sorted_data[coin_id].push(middle_item); }else { _regular_complete_sorted_data[coin_id].push(item); } }); if( !_is_coin_chart_data_loading ) { const _regular_formatted_complete_sorted_data = _regular_complete_sorted_data["coin_id"].map((element, i, array) => { const bitcoin_index = (_regular_complete_sorted_data["bitcoin"].length - array.length) + i; return { date: element.date, value: element.value, bitcoin: typeof _regular_complete_sorted_data["bitcoin"][bitcoin_index] === "undefined" ? 0: _regular_complete_sorted_data["bitcoin"][bitcoin_index].value }; }); this.setState({_regular_complete_sorted_data, _ticks_array, _regular_formatted_complete_sorted_data}); actions.trigger_loading_update(100); }else { this.setState({_regular_complete_sorted_data}); } }); }else { this.setState({_is_coin_chart_data_loading: false}, () => { actions.trigger_loading_update(100); }); } }; _set_coin_chart_data_time = (time) => { this.setState({ _coin_chart_data_time: time }, this._get_coin_chart_data); }; _set_coin_chart_data_type = (type) => { actions.jamy_update("happy", 500); this.setState({ _coin_chart_data_type: type }, this._get_coin_chart_data); }; _get_coin_chart_data_ticks = (graph_data, coin_chart_data_time) => { const { selected_locales_code } = this.state; if (!graph_data || !graph_data.length || !selected_locales_code) {return [];} const domain = [new Date(+graph_data[0].date), new Date(+graph_data[graph_data.length - 1].date)]; const scale = scaleTime().domain(domain).range([0, 1]); let ticks = []; let date_array = []; switch (coin_chart_data_time) { case "1": ticks = scale.ticks(utcHour, 1); ticks = ticks.filter((entry, index) => {return index % 2}); return ticks.map(entry => +entry); case "7": ticks = scale.ticks(utcDay, 1); return ticks.map(entry => +entry); case "30": let utc_day = utcWeek; switch(FIRST_WEEK_DAY_BY_COUNTRY[selected_locales_code.split("-")[1] || "US"]) { case "mon": utc_day = utcMonday; break; case "fri": utc_day = utcFriday; break; case "sat": utc_day = utcSaturday; break; case "sun": utc_day = utcSunday; break; } ticks = scale.ticks(utc_day, 1); return ticks.map(entry => +entry); case "180": ticks = scale.ticks(utcMonth, 1); return ticks.map(entry => +entry); case "360": ticks = scale.ticks(utcMonth, 1); return ticks.map(entry => +entry); case "max": ticks = scale.ticks(utcYear, 1); return ticks.map(entry => +entry); } }; _get_graph_data_with_ticks = (graph_data, ticks) => { if (!graph_data || !graph_data.length ) {return [];} const graph_data_map = new Map(graph_data.map((item) => [item.date, item])); ticks.forEach(function (item, index, array) { if(!graph_data_map.has(item)) { graph_data.push({date: item, value: "irregular"}); } }); return graph_data; }; _price_formatter = (price, compact = false, display_currency = true) => { const { selected_locales_code, selected_currency } = this.state; return display_currency ? price_formatter(price, selected_currency, selected_locales_code, compact): price_formatter(price, null, selected_locales_code, compact); }; _date_formatter = (date, precise = false) => { const { _coin_chart_data_time, selected_locales_code } = this.state; if(_coin_chart_data_time === "1") { return precise ? new Intl.DateTimeFormat(selected_locales_code, {hour: "numeric", minute: "numeric"}).format(new Date(date)): new Intl.DateTimeFormat(selected_locales_code, {hour: "numeric"}).format(new Date(date)); }else if(_coin_chart_data_time === "7"){ return precise ? new Intl.DateTimeFormat(selected_locales_code, {hour: "numeric", day: "numeric", month: "short"}).format(new Date(date)): new Intl.DateTimeFormat(selected_locales_code, {day: "numeric", month: "short"}).format(new Date(date)); }else if(_coin_chart_data_time === "30"){ return precise ? new Intl.DateTimeFormat(selected_locales_code, {hour: "numeric", day: "numeric", month: "short"}).format(new Date(date)): new Intl.DateTimeFormat(selected_locales_code, {day: "numeric", month: "short"}).format(new Date(date)); }else if(_coin_chart_data_time === "180"){ return precise ? new Intl.DateTimeFormat(selected_locales_code, {day: "numeric", month: "short"}).format(new Date(date)): new Intl.DateTimeFormat(selected_locales_code, {month: "short"}).format(new Date(date)); }else if(_coin_chart_data_time === "360"){ return precise ? new Intl.DateTimeFormat(selected_locales_code, {day: "numeric", month: "short", year: "numeric"}).format(new Date(date)): new Intl.DateTimeFormat(selected_locales_code, {month: "short", year: "numeric"}).format(new Date(date)); }else if(_coin_chart_data_time === "max"){ return precise ? new Intl.DateTimeFormat(selected_locales_code, {day: "numeric", month: "short", year: "numeric"}).format(new Date(date)): new Intl.DateTimeFormat(selected_locales_code, {month: "short", year: "numeric"}).format(new Date(date)); } }; _custom_tooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return ( <Card style={{padding: 12}}> <b>{this._date_formatter(label, true)}</b><br /> <span>{this._price_formatter(payload[0].payload.value)}</span> </Card> ); } return null; }; render() { const { classes, selected_locales_code, _regular_formatted_complete_sorted_data, _ticks_array, _coin_chart_data_time, _coin_chart_data_type, _is_coin_chart_data_loading, coin_id } = this.state; return ( <div className={classes.fullHeight}> <div className={classes.overlay} style={_is_coin_chart_data_loading ? {}: {display: "none"}}> <div className={classes.circularProgressContainer}> <CircularProgress color="inherit" /> </div> </div> <Fade in> <Card className={classes.fullHeight}> <CardContent className={classes.flowRoot}> <ButtonGroup size="small" aria-label="Price and market cap buttons" className={classes.floatLeft}> <Button className={_coin_chart_data_type === "prices" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_type("prices")}>{t("words.price")}</Button> <Button className={_coin_chart_data_type === "market_caps" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_type("market_caps")}>{t("words.cap", {AED: true})}</Button> </ButtonGroup> <ButtonGroup size="small" aria-label="Chart time range button" className={classes.floatRight}> <Button className={_coin_chart_data_time === "1" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_time("1")}>{t("words.24h")}</Button> <Button className={_coin_chart_data_time === "7" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_time("7")}>{t("words.7d")}</Button> <Button className={_coin_chart_data_time === "30" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_time("30")}>{t("words.30d")}</Button> <Button className={_coin_chart_data_time === "180" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_time("180")}>{t("words.180d")}</Button> <Button className={_coin_chart_data_time === "360" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_time("360")}>{t("words.1y")}</Button> <Button className={_coin_chart_data_time === "max" ? classes.contrastButton: null} onClick={() => this._set_coin_chart_data_time("max")}>{t("words.max")}</Button> </ButtonGroup> </CardContent> <CardContent className={classes.chartCardContent}> <Fade in timeout={300}> <div className={classes.chart}> { _regular_formatted_complete_sorted_data.length ? <ResponsiveContainer> <AreaChart data={_regular_formatted_complete_sorted_data} > <defs> <linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1"> <stop offset={1} stopColor="#1c1882" stopOpacity="0.2"></stop> </linearGradient> <linearGradient id="colorBtc" x1="0" y1="0" x2="0" y2="1"> <stop offset={1} stopColor="#1c1882" stopOpacity="0"></stop> </linearGradient> </defs> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="date" domain={['dataMin', 'dataMax']} interval={_coin_chart_data_time === "max" ? "preserveEnd": 0} angle={60} height={75} dy={10} textAnchor="start" tickFormatter={date => this._date_formatter(date)} ticks={_ticks_array} tickCount={_ticks_array.length}/> <YAxis yAxisId="left" dataKey="value" type={"number"} tickFormatter={value => this._price_formatter(value, true, false)}/> <YAxis yAxisId="right" orientation="right" dataKey="bitcoin" type={"number"} tickFormatter={value => this._price_formatter(value, true, false)}/> <Tooltip content={data => this._custom_tooltip(data)}/> <Area type="monotone" yAxisId="right" stroke="#c6c6d9" fill="url(#colorBtc)" dataKey="bitcoin" strokeLinecap="round" dot={false} strokeWidth={1.5} activeDot={{ strokeWidth: 0, r: 3 }}/> <Area type="monotone" yAxisId="left" stroke="#1c1882" fill="url(#colorUv)" dataKey="value" strokeLinecap="round" dot={false} strokeWidth={2.5} activeDot={<ChartDot dotColor={"#1c1882"}/>}/> </AreaChart> </ResponsiveContainer>: <Skeleton className={classes.chart} /> } </div> </Fade> </CardContent> </Card> </Fade> </div> ); } } export default withStyles(styles)(CoinChartsChart);