diff --git a/components/LineChart.js b/components/LineChart.js new file mode 100644 index 00000000..eca5ef89 --- /dev/null +++ b/components/LineChart.js @@ -0,0 +1,349 @@ +import { c, t, msgid } from 'ttag'; +import { max, curveMonotoneX, scaleTime, scaleLinear, line } from 'd3'; +import { startOfDay } from 'date-fns'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles(theme => ({ + lineChartContainer: { + position: 'relative', + }, + plot: { + fontSize: '10px', + color: theme.palette.secondary[300], + lineHeight: '10px', + '& .line': { + strokeWidth: '2px', + fill: 'none', + }, + '& .gridline': { + strokeWidth: 1, + stroke: theme.palette.secondary[100], + }, + '& .leftAxis': { + color: theme.palette.info.dark, + fontWeight: 500, + }, + '& .webLine': { + stroke: theme.palette.info.main, + }, + '& .rightAxis': { + color: theme.palette.primary[900], + fontWeight: 500, + }, + '& .chatbotLine': { + stroke: theme.palette.primary.main, + }, + }, + wrapper: { + display: 'inline-block', + position: 'absolute', + cursor: 'crosshair', + + '& .hitbox': { + position: 'absolute', + zIndex: 1000, + + '&:hover .dot, &:hover .vLine, &:hover .tooltip': { + display: 'inline-block', + }, + }, + + '& .vLine': { + width: '1px', + borderLeft: `1px solid ${theme.palette.error.main}`, + position: 'absolute', + display: 'none', + }, + '& .dot': { + width: '10px', + height: '10px', + borderRadius: '5px', + position: 'absolute', + display: 'none', + + '&.web': { + background: theme.palette.info.main, + }, + '&.chatbot': { + background: theme.palette.primary.main, + }, + }, + '& .tooltip': { + background: theme.palette.secondary[500], + borderRadius: '2px', + display: 'none', + fontSize: '12px', + lineHeight: '20px', + padding: '5px 10px', + textAlign: 'center', + whiteSpace: 'nowrap', + position: 'absolute', + + '& .tooltip-title': { + borderBottom: `1px solid ${theme.palette.secondary[400]}`, + color: 'white', + }, + '& .tooltip-text': { + color: 'white', + margin: 0, + }, + '& .tooltip-subtitle': { + color: theme.palette.secondary[300], + margin: 0, + }, + + '&.left': { + left: '40px', + }, + '&.right': { + right: '40px', + }, + }, + }, +})); + +// round up to the nearest 10 +const getMax = (list, getter) => + Math.max(Math.ceil(max(list, getter) / 10) * 10, 10); + +/** + * Given analytics stat, performs computations needed to plot a line chart. + + * @param {array} dataset List of data of the form {date, webVisit, lineVisit} + * @param {number} width Width of plot area + * @param {number} height Height of plot area + + * @return { + xScale: {d3.scaleTime} + yScaleWeb: {d3.scaleLinear} + yScaleLine: {d3.scaleLinear} + chatbotDots: {array} List of corresponding positions for each chatbot entry + chatbotLine: {d3.line} + webDots: {array} List of corresponding positions for each web entry + webLine: {d3.line} + } + */ +const computeChartData = (dataset, width, height) => { + const maxWebVisit = getMax(dataset, d => d.webVisit); + const maxLineVisit = getMax(dataset, d => d.lineVisit); + + const xScale = scaleTime() + .domain([dataset[0].date, dataset[dataset.length - 1].date]) + .range([0, width]); + + const yScaleWeb = scaleLinear() + .domain([0, maxWebVisit]) + .range([height, 0]); + + const yScaleLine = scaleLinear() + .domain([0, maxLineVisit]) + .range([height, 0]); + + const webLine = line() + .x(d => xScale(d.date)) + .y(d => yScaleWeb(d.webVisit)) + .curve(curveMonotoneX); + + const chatbotLine = line() + .x(d => xScale(d.date)) + .y(d => yScaleLine(d.lineVisit)) + .curve(curveMonotoneX); + + const webDots = dataset.map(d => ({ + value: d.webVisit, + date: d.date, + cx: xScale(d.date), + cy: yScaleWeb(d.webVisit), + })); + const chatbotDots = dataset.map(d => ({ + value: d.lineVisit, + date: d.date, + cx: xScale(d.date), + cy: yScaleLine(d.lineVisit), + })); + + return { + xScale, + yScaleWeb, + yScaleLine, + chatbotDots, + chatbotLine, + webDots, + webLine, + }; +}; + +/* Renders gridline and axis tick label. */ +function TickGroup({ x, y, gridline, text }) { + return ( + + + + {text.text} + + + ); +} +/* Populates a list of tick values given a scale and number of ticks wanted. */ +const getTicks = (scale, roundFn, tickNum) => { + const [max, min] = scale.domain(); + const diff = (max - min) / (tickNum - 1); + let ticks = []; + for (let i = 0; i < tickNum; i++) { + ticks.push(roundFn(max - diff * i)); + } + return ticks; +}; + +const formatDate = (date, withYear = false) => { + const d = new Date(date); + const dateString = `${d.getMonth() + 1}/${d.getDate()}`; // MM/DD + if (withYear) { + return `${d.getFullYear()}/${dateString}`; + } + return dateString; +}; + +function plotTicks({ scale, x, y, text, roundFn, tickNum, gridline }) { + const ticks = getTicks(scale, roundFn, tickNum); + return ticks.map((tick, i) => ( + + )); +} + +const getVisitText = visitNum => + c('LineChart').ngettext( + msgid`${visitNum} time`, + `${visitNum} times`, + visitNum + ); + +export default function LineChart({ dataset, width, margin }) { + const classes = useStyles(); + const height = Math.max(Math.ceil(width / 6), 75); + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const boxWidth = innerWidth / (dataset.length - 1); + + const chartData = computeChartData(dataset, innerWidth, innerHeight); + return ( +
+ + + + {plotTicks({ + scale: chartData.yScaleWeb, + tickNum: 3, + x: () => 0, + y: (scale, tick) => scale(tick), + roundFn: value => Math.ceil(value), + text: tick => ({ x: -9, y: 5, text: tick }), + gridline: { x2: innerWidth }, + })} + + + + {plotTicks({ + scale: chartData.yScaleLine, + tickNum: 3, + x: () => 0, + y: (scale, tick) => scale(tick), + roundFn: value => Math.ceil(value), + text: tick => ({ x: 8, y: 5, text: tick }), + gridline: {}, + })} + + + + {plotTicks({ + scale: chartData.xScale, + tickNum: 11, + x: (scale, tick) => scale(tick), + y: () => 0, + roundFn: d => startOfDay(d), + text: tick => ({ x: 0, y: 20, text: formatDate(tick) }), + gridline: { y2: -innerHeight }, + })} + + + + + + + + + + +
+ {dataset.map((d, i) => ( +
+
+
+
+
+
{formatDate(d.date, true)}
+
+

{t`Web Visit`}

+

{getVisitText(d.webVisit)}

+

{t`Line Inquiry`}

+

{getVisitText(d.lineVisit)}

+
+
+
+ ))} +
+
+ ); +} diff --git a/components/LineChart.stories.js b/components/LineChart.stories.js new file mode 100644 index 00000000..306e8c8f --- /dev/null +++ b/components/LineChart.stories.js @@ -0,0 +1,48 @@ +import React from 'react'; + +import LineChart from './LineChart'; +export default { + title: 'LineChart', + component: 'LineChart', +}; + +const data = [ + { date: new Date('2020-07-07'), webVisit: 277, lineVisit: 5 }, + { date: new Date('2020-07-08'), webVisit: 2969, lineVisit: 29 }, + { date: new Date('2020-07-09'), webVisit: 14865, lineVisit: 84 }, + { date: new Date('2020-07-10'), webVisit: 18923, lineVisit: 71 }, + { date: new Date('2020-07-11'), webVisit: 8213, lineVisit: 44 }, + { date: new Date('2020-07-12'), webVisit: 2981, lineVisit: 29 }, + { date: new Date('2020-07-13'), webVisit: 927, lineVisit: 24 }, + { date: new Date('2020-07-14'), webVisit: 366, lineVisit: 6 }, + { date: new Date('2020-07-15'), webVisit: 360, lineVisit: 6 }, + { date: new Date('2020-07-16'), webVisit: 434, lineVisit: 8 }, + { date: new Date('2020-07-17'), webVisit: 227, lineVisit: 13 }, + { date: new Date('2020-07-18'), webVisit: 78, lineVisit: 2 }, + { date: new Date('2020-07-19'), webVisit: 92, lineVisit: 2 }, + { date: new Date('2020-07-20'), webVisit: 50, lineVisit: 2 }, + { date: new Date('2020-07-21'), webVisit: 50, lineVisit: 0 }, + { date: new Date('2020-07-22'), webVisit: 42, lineVisit: 0 }, + { date: new Date('2020-07-23'), webVisit: 55, lineVisit: 0 }, + { date: new Date('2020-07-24'), webVisit: 45, lineVisit: 0 }, + { date: new Date('2020-07-25'), webVisit: 36, lineVisit: 0 }, + { date: new Date('2020-07-26'), webVisit: 36, lineVisit: 0 }, + { date: new Date('2020-07-27'), webVisit: 18, lineVisit: 0 }, + { date: new Date('2020-07-28'), webVisit: 18, lineVisit: 2 }, + { date: new Date('2020-07-29'), webVisit: 19, lineVisit: 0 }, + { date: new Date('2020-07-30'), webVisit: 19, lineVisit: 0 }, + { date: new Date('2020-07-31'), webVisit: 1, lineVisit: 0 }, + { date: new Date('2020-08-01'), webVisit: 6, lineVisit: 0 }, + { date: new Date('2020-08-02'), webVisit: 3, lineVisit: 0 }, + { date: new Date('2020-08-03'), webVisit: 6, lineVisit: 0 }, + { date: new Date('2020-08-04'), webVisit: 4, lineVisit: 0 }, + { date: new Date('2020-08-05'), webVisit: 6, lineVisit: 0 }, + { date: new Date('2020-08-06'), webVisit: 0, lineVisit: 0 }, +]; +export const Normal = () => ( + +); diff --git a/components/TrendPlot.js b/components/TrendPlot.js new file mode 100644 index 00000000..dfec5318 --- /dev/null +++ b/components/TrendPlot.js @@ -0,0 +1,164 @@ +import { t } from 'ttag'; +import { useState } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { makeStyles } from '@material-ui/core/styles'; +import { + startOfDay, + eachDayOfInterval, + subDays, + addDays, + differenceInDays, +} from 'date-fns'; +import LineChart from './LineChart'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import Box from '@material-ui/core/Box'; +import Hidden from '@material-ui/core/Hidden'; + +const CHART_DURATION = 31; + +const margin = { top: 10, left: 40, right: 20, bottom: 20 }; + +const useStyles = makeStyles(theme => ({ + label: { + fontSize: 14, + display: 'inline-block', + margin: '0 5px', + + [theme.breakpoints.down('xs')]: { + margin: '0 2px', + }, + + '&.plotLabel': { + color: theme.palette.secondary[500], + }, + '&.webLabel': { + color: theme.palette.info.main, + }, + '&.lineLabel': { + color: theme.palette.primary.main, + }, + '&.totalLabel': { + color: theme.palette.secondary[200], + textAlign: 'right', + marginTop: '-7px', + + '& .MuiSvgIcon-root': { + transform: 'translate(0px, 6px)', + width: 14, + }, + }, + }, + root: { + flex: 1, + height: '100%', + marginBottom: '20px', + }, +})); + +/** + * Push zero entries to dataset for dates between `start` and `end`. + */ +const fillEmptyDates = (start, end, dataset) => { + eachDayOfInterval({ start, end }).forEach(date => + dataset.push({ date, webVisit: 0, lineVisit: 0 }) + ); +}; + +/** + * Given analytics stat, populate dataset for last 31 days. + */ +const populateChartData = data => { + let dataset = []; + const endDate = startOfDay(new Date()); + const startDate = subDays(endDate, CHART_DURATION - 1); + const firstDateInData = + data && data.length > 0 + ? startOfDay(new Date(data[0].date)) + : addDays(endDate, 1); + let lastProcessedDate; + let totalWebVisits = 0; + let totalLineVisits = 0; + + // fill in zeros if first date in data is less than 31 days ago. + if (firstDateInData > startDate) { + fillEmptyDates(startDate, subDays(firstDateInData, 1), dataset); + lastProcessedDate = subDays(firstDateInData, 1); + } + + if (data) { + data.forEach(d => { + const date = startOfDay(new Date(d.date)); + // if there's a gap between dates, fill with zeros. + if (differenceInDays(date, lastProcessedDate) > 1) { + fillEmptyDates( + addDays(lastProcessedDate, 1), + subDays(date, 1), + dataset + ); + } + const webVisit = +d.webVisit || 0; + const lineVisit = +d.lineVisit || 0; + dataset.push({ date, webVisit, lineVisit }); + totalWebVisits += webVisit; + totalLineVisits += lineVisit; + lastProcessedDate = date; + }); + } + + // if last processed date is before current date, fill the gap with zeros. + if (lastProcessedDate < endDate) { + fillEmptyDates(addDays(lastProcessedDate, 1), endDate, dataset); + } + + return { dataset, totalLineVisits, totalWebVisits }; +}; + +export default function TrendPlot({ data }) { + const classes = useStyles(); + const [showPlot, setPlotShow] = useState(true); + + const { dataset, totalLineVisits, totalWebVisits } = populateChartData(data); + const totalVisits = totalWebVisits + totalLineVisits; + + return ( +
+ + {t`past 31 days`} + + + {t`Web Visit: ${totalWebVisits}`} + + + {t`Line Inquery: ${totalLineVisits}`} + + + + + {t`Web: ${totalWebVisits}`} + + + {t`Line: ${totalLineVisits}`} + + + + {t`Total Visit: ${totalVisits}`} + {showPlot ? ( + setPlotShow(false)} /> + ) : ( + setPlotShow(true)} /> + )} + + + + {showPlot && ( + + {({ width }) => ( + + )} + + )} + +
+ ); +} diff --git a/components/TrendPlot.stories.js b/components/TrendPlot.stories.js new file mode 100644 index 00000000..2289a4f8 --- /dev/null +++ b/components/TrendPlot.stories.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { startOfDay, eachDayOfInterval, subDays } from 'date-fns'; +import TrendPlot from './TrendPlot'; +export default { + title: 'TrendPlot', + component: 'TrendPlot', +}; +const stats = [ + null, + null, + { webVisit: '2969', lineVisit: '29' }, + { webVisit: '14865', lineVisit: '84' }, + { webVisit: '18923', lineVisit: '71' }, + { webVisit: '8213', lineVisit: '44' }, + { webVisit: '2981', lineVisit: '29' }, + { webVisit: '927', lineVisit: '24' }, + { webVisit: '366', lineVisit: '6' }, + { webVisit: '360', lineVisit: '6' }, + { webVisit: '434', lineVisit: '8' }, + { webVisit: '227', lineVisit: '13' }, + { webVisit: '78', lineVisit: '2' }, + { webVisit: '92', lineVisit: '2' }, + { webVisit: '50', lineVisit: '2' }, + { webVisit: '50' }, + { webVisit: '42' }, + { webVisit: '55' }, + { webVisit: '45' }, + { webVisit: '36' }, + { webVisit: '36' }, + { webVisit: '18' }, + { webVisit: '18', lineVisit: '2' }, + { webVisit: '19' }, + { webVisit: '19' }, + null, + { webVisit: '2' }, + { webVisit: '6' }, + { webVisit: '3' }, + { webVisit: '6' }, +]; + +const populateData = () => { + let data = []; + const today = startOfDay(new Date()); + eachDayOfInterval({ + start: subDays(today, 29), + end: subDays(today, 1), + }).forEach((date, i) => { + if (stats[i]) + data.push({ ...stats[i], date: date.toISOString().substr(0, 10) }); + }); + return data; +}; + +export const Normal = () => ( +
+ +
+); diff --git a/components/__snapshots__/LineChart.stories.storyshot b/components/__snapshots__/LineChart.stories.storyshot new file mode 100644 index 00000000..127129aa --- /dev/null +++ b/components/__snapshots__/LineChart.stories.storyshot @@ -0,0 +1,3030 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots LineChart Normal 1`] = ` + +
+ + + + + + + + 0 + + + + + + + + 9465 + + + + + + + + 18930 + + + + + + + + + + 0 + + + + + + + + 45 + + + + + + + + 90 + + + + + + + + + + 7/7 + + + + + + + + 7/10 + + + + + + + + 7/13 + + + + + + + + 7/16 + + + + + + + + 7/19 + + + + + + + + 7/22 + + + + + + + + 7/25 + + + + + + + + 7/28 + + + + + + + + 7/31 + + + + + + + + 8/3 + + + + + + + + 8/6 + + + + + + + + + + + + +
+
+
+
+
+
+
+ 2020/7/7 +
+
+

+ Web Visit +

+

+ 277 times +

+

+ Line Inquiry +

+

+ 5 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/8 +
+
+

+ Web Visit +

+

+ 2969 times +

+

+ Line Inquiry +

+

+ 29 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/9 +
+
+

+ Web Visit +

+

+ 14865 times +

+

+ Line Inquiry +

+

+ 84 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/10 +
+
+

+ Web Visit +

+

+ 18923 times +

+

+ Line Inquiry +

+

+ 71 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/11 +
+
+

+ Web Visit +

+

+ 8213 times +

+

+ Line Inquiry +

+

+ 44 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/12 +
+
+

+ Web Visit +

+

+ 2981 times +

+

+ Line Inquiry +

+

+ 29 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/13 +
+
+

+ Web Visit +

+

+ 927 times +

+

+ Line Inquiry +

+

+ 24 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/14 +
+
+

+ Web Visit +

+

+ 366 times +

+

+ Line Inquiry +

+

+ 6 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/15 +
+
+

+ Web Visit +

+

+ 360 times +

+

+ Line Inquiry +

+

+ 6 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/16 +
+
+

+ Web Visit +

+

+ 434 times +

+

+ Line Inquiry +

+

+ 8 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/17 +
+
+

+ Web Visit +

+

+ 227 times +

+

+ Line Inquiry +

+

+ 13 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/18 +
+
+

+ Web Visit +

+

+ 78 times +

+

+ Line Inquiry +

+

+ 2 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/19 +
+
+

+ Web Visit +

+

+ 92 times +

+

+ Line Inquiry +

+

+ 2 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/20 +
+
+

+ Web Visit +

+

+ 50 times +

+

+ Line Inquiry +

+

+ 2 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/21 +
+
+

+ Web Visit +

+

+ 50 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/22 +
+
+

+ Web Visit +

+

+ 42 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/23 +
+
+

+ Web Visit +

+

+ 55 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/24 +
+
+

+ Web Visit +

+

+ 45 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/25 +
+
+

+ Web Visit +

+

+ 36 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/26 +
+
+

+ Web Visit +

+

+ 36 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/27 +
+
+

+ Web Visit +

+

+ 18 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/28 +
+
+

+ Web Visit +

+

+ 18 times +

+

+ Line Inquiry +

+

+ 2 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/29 +
+
+

+ Web Visit +

+

+ 19 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/30 +
+
+

+ Web Visit +

+

+ 19 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/7/31 +
+
+

+ Web Visit +

+

+ 1 time +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/8/1 +
+
+

+ Web Visit +

+

+ 6 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/8/2 +
+
+

+ Web Visit +

+

+ 3 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/8/3 +
+
+

+ Web Visit +

+

+ 6 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/8/4 +
+
+

+ Web Visit +

+

+ 4 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/8/5 +
+
+

+ Web Visit +

+

+ 6 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+
+
+
+
+ 2020/8/6 +
+
+

+ Web Visit +

+

+ 0 times +

+

+ Line Inquiry +

+

+ 0 times +

+
+
+
+
+
+ +`; diff --git a/components/__snapshots__/TrendPlot.stories.storyshot b/components/__snapshots__/TrendPlot.stories.storyshot new file mode 100644 index 00000000..5988821a --- /dev/null +++ b/components/__snapshots__/TrendPlot.stories.storyshot @@ -0,0 +1,217 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots TrendPlot Normal 1`] = ` +
+ +
+
+
+ past 31 days +
+ + + + + + +
+ Total Visit: 51156 + +
+
+
+ +
+ +
+
+ +
+`; diff --git a/i18n/zh_TW.po b/i18n/zh_TW.po index cd3038d7..b604e880 100644 --- a/i18n/zh_TW.po +++ b/i18n/zh_TW.po @@ -1087,4 +1087,53 @@ msgstr "等你來答" #: components/AppLayout/AppHeader.js:188 msgctxt "App header" msgid "Forum" -msgstr "討論區" \ No newline at end of file +msgstr "討論區" + +#: components/LineChart.js:221 +msgctxt "LineChart" +msgid "${ visitNum } time" +msgid_plural "${ visitNum } times" +msgstr[0] "${ visitNum } 次" +msgstr[1] "${ visitNum } 次" + +#: components/LineChart.js:329 +msgid "Web Visit" +msgstr "網頁瀏覽" + +#: components/LineChart.js:333 +msgid "Line Inquiry" +msgstr "Line 詢問" + +#: components/LineChart.js:343 +#: components/LineChart.js:345 +msgid "Line" +msgstr "詢問" + +#: components/TrendPlot.js:127 +msgid "past 31 days" +msgstr "近31日" + +#: components/TrendPlot.js:130 +#, javascript-format +msgid "Web Visit: ${ totalWebVisits }" +msgstr "網頁瀏覽 ${ totalWebVisits } 次" + +#: components/TrendPlot.js:138 +#, javascript-format +msgid "Web: ${ totalWebVisits }" +msgstr "瀏覽 ${ totalWebVisits } 次" + +#: components/TrendPlot.js:133 +#, javascript-format +msgid "Line Inquery: ${ totalLineVisits }" +msgstr "Line 詢問 ${ totalLineVisits } 次" + +#: components/TrendPlot.js:141 +#, javascript-format +msgid "Line: ${ totalLineVisits }" +msgstr "詢問 ${ totalLineVisits } 次" + +#: components/TrendPlot.js:145 +#, javascript-format +msgid "Total Visit: ${ totalVisits }" +msgstr "${ totalVisits } 次瀏覽" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d34360a3..0cb1d777 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11261,6 +11261,270 @@ "type": "^1.0.1" } }, + "d3": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==" + }, + "d3-brush": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.5.tgz", + "integrity": "sha512-rEaJ5gHlgLxXugWjIkolTA0OyMvw8UWU1imYXy1v642XyyswmI1ybKOv05Ft+ewq+TFmdliD3VuK0pRp1VT/5A==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz", + "integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ==" + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz", + "integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw==" + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==" + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz", + "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==", + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==" + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -23615,6 +23879,11 @@ } } }, + "react-virtualized-auto-sizer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz", + "integrity": "sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==" + }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -24293,6 +24562,11 @@ "aproba": "^1.1.1" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", diff --git a/package.json b/package.json index 355162b1..56a88835 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "clsx": "^1.0.4", "copy-to-clipboard": "^3.3.1", "core-js": "^3.6.5", + "d3": "^5.16.0", "date-fns": "^2.0.1", "dotenv": "^8.0.0", "emoji-mart": "^3.0.0", @@ -44,6 +45,7 @@ "pm2": "^4.2.3", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-virtualized-auto-sizer": "^1.0.2", "rollbar": "^2.14.4", "stackimpact": "^1.3.23", "ttag": "^1.7.22" diff --git a/pages/article/[id].js b/pages/article/[id].js index a7923238..ac839ee2 100644 --- a/pages/article/[id].js +++ b/pages/article/[id].js @@ -26,7 +26,7 @@ import NewReplySection from 'components/NewReplySection'; import ArticleInfo from 'components/ArticleInfo'; import ArticleCategories from 'components/ArticleCategories'; import cx from 'clsx'; -import Trendline from 'components/Trendline'; +import TrendPlot from 'components/TrendPlot'; const useStyles = makeStyles(theme => ({ root: { @@ -177,6 +177,11 @@ const LOAD_ARTICLE = gql` ...ArticleCategoryData ...AddCategoryDialogData } + stats { + date + webVisit + lineVisit + } } } ${Hyperlinks.fragments.HyperlinkData} @@ -342,7 +347,7 @@ function ArticlePage() { ({ status }) => status === 'NORMAL' )} /> - +