diff --git a/client/.storybook/preview.js b/client/.storybook/preview.js
index 00a21d01f4..05544d95d5 100644
--- a/client/.storybook/preview.js
+++ b/client/.storybook/preview.js
@@ -2,5 +2,12 @@ import "bootstrap/dist/css/bootstrap.css"
import "index.css"
export const parameters = {
- actions: { argTypesRegex: "^on[A-Z].*" }
+ actions: { argTypesRegex: "^on[A-Z].*" },
+ options: {
+ // Sort stories alphabetically
+ storySort: (a, b) =>
+ a[1].kind === b[1].kind
+ ? 0
+ : a[1].id.localeCompare(b[1].id, undefined, { numeric: true })
+ }
}
diff --git a/client/config/webpack.client.prod.js b/client/config/webpack.client.prod.js
index c4e2b540b3..e4927cc123 100644
--- a/client/config/webpack.client.prod.js
+++ b/client/config/webpack.client.prod.js
@@ -24,7 +24,7 @@ const clientConfig = merge.merge(common.clientConfig, {
new TerserPlugin({
cache: true,
parallel: true,
- sourceMap: false // TODO: disabled until SourceMapDevToolPlugin supports caching in webpack 5
+ sourceMap: true // TODO: disabled until SourceMapDevToolPlugin supports caching in webpack 5
})
],
splitChunks: {
diff --git a/client/src/components/Chart.js b/client/src/components/Chart.js
new file mode 100644
index 0000000000..1270409cd5
--- /dev/null
+++ b/client/src/components/Chart.js
@@ -0,0 +1,53 @@
+import useLayout from "layouts/useLayout"
+import { LAYOUT_AGGREGATORS } from "layouts/utils"
+import PropTypes from "prop-types"
+import React, { useState } from "react"
+
+const Chart = ({
+ items,
+ layoutType,
+ widgetElement: Widget,
+ widgetConfig,
+ style
+}) => {
+ const aggregator = LAYOUT_AGGREGATORS[layoutType]
+ const [aggregatedItems] = aggregator(items)
+ const [HeaderElement, layout, initViewState, ref] = useLayout(layoutType)
+
+ const [viewState, setViewState] = useState(initViewState)
+
+ return (
+ <>
+
+
+ >
+ )
+}
+Chart.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.object),
+ layoutType: PropTypes.string,
+ widgetElement: PropTypes.func,
+ style: PropTypes.object,
+ widgetConfig: PropTypes.object
+}
+export default Chart
diff --git a/client/src/components/DateChart.js b/client/src/components/DateChart.js
new file mode 100644
index 0000000000..2b284fde0b
--- /dev/null
+++ b/client/src/components/DateChart.js
@@ -0,0 +1,43 @@
+import PropTypes from "prop-types"
+import React from "react"
+
+const DateChart = ({
+ items,
+ layout,
+ widgetElement: Widget,
+ viewState: viewDate,
+ widgetConfig
+}) => {
+ return (
+ <>
+ {items.map(item => {
+ const boundingRect = layout(item, viewDate)
+ // if it isn't in the layout ( e.g different year, month)
+ if (!boundingRect) {
+ return null
+ }
+ return (
+
+
+
+ )
+ })}
+ >
+ )
+}
+DateChart.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.object),
+ layout: PropTypes.func,
+ widgetElement: PropTypes.func,
+ viewState: PropTypes.object,
+ widgetConfig: PropTypes.object
+}
+
+export default DateChart
diff --git a/client/src/components/GeoChart.js b/client/src/components/GeoChart.js
new file mode 100644
index 0000000000..fefbed6e97
--- /dev/null
+++ b/client/src/components/GeoChart.js
@@ -0,0 +1,10 @@
+import PropTypes from "prop-types"
+import React from "react"
+
+const GeoChart = ({ items }) => {
+ return <>{items.title}>
+}
+GeoChart.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.object)
+}
+export default GeoChart
diff --git a/client/src/components/HeatMap.js b/client/src/components/HeatMap.js
new file mode 100644
index 0000000000..b552771a27
--- /dev/null
+++ b/client/src/components/HeatMap.js
@@ -0,0 +1,46 @@
+import HeatWidget from "components/aggregations/HeatWidget"
+import Chart from "components/Chart"
+import { LAYOUT_TYPES } from "layouts/utils"
+import PropTypes from "prop-types"
+import React, { useState } from "react"
+
+// TODO: this config can come from layouts/utils or be input to HeatMap or input from user
+const heatConfig = {
+ low: 1,
+ mid: 3,
+ bgColor: "red",
+ textColor: "black"
+}
+const HeatMap = ({ items, style }) => {
+ const [layout, setLayout] = useState(LAYOUT_TYPES.YEAR)
+ return (
+
+
+
+
+
+
+
+ )
+}
+HeatMap.propTypes = {
+ items: PropTypes.arrayOf(PropTypes.object),
+ style: PropTypes.object
+}
+
+export default HeatMap
diff --git a/client/src/components/LayoutHeader.js b/client/src/components/LayoutHeader.js
new file mode 100644
index 0000000000..f880788cb8
--- /dev/null
+++ b/client/src/components/LayoutHeader.js
@@ -0,0 +1,87 @@
+import moment from "moment"
+import PropTypes from "prop-types"
+import React from "react"
+import { Button } from "react-bootstrap"
+
+export const DateHeader = ({ viewDate, setViewDate, dateScale, format }) => {
+ return (
+
+ {viewDate.format(format)}
+
+
+
+
+
+
+ )
+}
+
+DateHeader.propTypes = {
+ viewDate: PropTypes.object,
+ setViewDate: PropTypes.func,
+ dateScale: PropTypes.string,
+ format: PropTypes.string
+}
+
+export const YearHeader = ({ viewState, setViewState }) => {
+ return (
+
+ )
+}
+
+YearHeader.propTypes = {
+ viewState: PropTypes.object,
+ setViewState: PropTypes.func
+}
+
+export const MonthHeader = ({ viewState, setViewState }) => {
+ return (
+
+ )
+}
+
+MonthHeader.propTypes = {
+ viewState: PropTypes.object,
+ setViewState: PropTypes.func
+}
+
+// FIXME: fix when geolayout ready
+export const GeoHeader = ({
+ viewState: viewLocation,
+ setViewState: setViewLocation
+}) => {
+ console.log(setViewLocation)
+
+ return
+}
+
+GeoHeader.propTypes = {
+ viewState: PropTypes.object,
+ setViewState: PropTypes.func
+}
diff --git a/client/src/components/aggregations/HeatWidget.js b/client/src/components/aggregations/HeatWidget.js
new file mode 100644
index 0000000000..d6b2cd2078
--- /dev/null
+++ b/client/src/components/aggregations/HeatWidget.js
@@ -0,0 +1,43 @@
+import { numOfEventsToHeatBgc } from "components/aggregations/utils"
+import PropTypes from "prop-types"
+import React from "react"
+
+// TODO: add color scales
+// TODO: api will probably change
+/**
+ * @param {object} item - aggregated item in the shape {aggregationKey: string, [aggregationKey]: object, numOfEvents: number}
+ * @param {object} dimensions - what view types you want to use
+ * @param {object} heatConfig - object in the form {low:number, mid:number, bgColor:string, textColor: string}
+ * 4 levels of tone for event count: if = 0, if <= low, if <= mid, if > mid
+ */
+const HeatWidget = ({ item, dimensions, widgetConfig: heatConfig }) => {
+ const bgc = numOfEventsToHeatBgc(item.numOfEvents, heatConfig)
+
+ return (
+ <>
+
+
+ {item.numOfEvents}
+
+ >
+ )
+}
+HeatWidget.propTypes = {
+ item: PropTypes.object,
+ dimensions: PropTypes.object,
+ widgetConfig: PropTypes.object
+}
+export default HeatWidget
diff --git a/client/src/components/aggregations/utils.js b/client/src/components/aggregations/utils.js
index c31ade1df3..31741d297b 100644
--- a/client/src/components/aggregations/utils.js
+++ b/client/src/components/aggregations/utils.js
@@ -220,3 +220,30 @@ export function reportsToEvents(reports) {
}
})
}
+export const COLOR_NAMES_TO_RGB = {
+ red: "rgb(155, 0, 0, ",
+ blue: "rgb(0, 0, 155, ",
+ green: "rgb(0, 155, 0, ",
+ pink: "rgb(194, 31, 169, ",
+ orange: "rgb(199, 135, 6, ",
+ white: "rgb(255, 255, 255, ",
+ brown: "rgb(128, 57, 30, "
+}
+
+/**
+ * There are 4 levels, [none, low, mid, high], none=0 assumed, we need two values: low, mid
+ * Available colors ["red", "blue", "green", "pink", "orange", "brown"]
+ * @param {number} numOfEvents - number of items to scale
+ * @param {object} scale - example object: {low: 3, mid: 6, bgColor: "red"}
+ */
+export function numOfEventsToHeatBgc(numOfEvents, scale) {
+ if (numOfEvents === 0) {
+ return "transparent"
+ } else if (numOfEvents <= scale.low) {
+ return `${COLOR_NAMES_TO_RGB[scale.bgColor]}0.25)`
+ } else if (numOfEvents <= scale.mid) {
+ return `${COLOR_NAMES_TO_RGB[scale.bgColor]}0.5)`
+ } else {
+ return `${COLOR_NAMES_TO_RGB[scale.bgColor]}1.0)`
+ }
+}
diff --git a/client/src/layouts.js b/client/src/layouts.js
new file mode 100644
index 0000000000..5e87d6e47a
--- /dev/null
+++ b/client/src/layouts.js
@@ -0,0 +1,33 @@
+import geoLayout from "layouts/geoLayout"
+import monthLayout from "layouts/monthLayout"
+import { LAYOUT_TYPES } from "layouts/utils"
+import yearLayout from "layouts/yearLayout"
+import * as d3 from "d3"
+
+const LAYOUTS = {
+ [LAYOUT_TYPES.GEO]: geoLayout(
+ d3.geoMercator().fitSize([1000, 500], {
+ type: "FeatureCollection",
+ features: [
+ {
+ type: "Feature",
+ geometry: {
+ type: "Polygon",
+ coordinates: [
+ [
+ [45, 5],
+ [55, 5],
+ [55, 15],
+ [45, 15]
+ ]
+ ]
+ }
+ }
+ ]
+ })
+ ),
+ [LAYOUT_TYPES.MONTH]: monthLayout,
+ [LAYOUT_TYPES.YEAR]: yearLayout
+}
+
+export default LAYOUTS
diff --git a/client/src/layouts/geoLayout.js b/client/src/layouts/geoLayout.js
new file mode 100644
index 0000000000..b311801366
--- /dev/null
+++ b/client/src/layouts/geoLayout.js
@@ -0,0 +1,8 @@
+const geoLayout = projection => (item, dimensions, viewLocation) => ({
+ x: projection(item.coordinates)[0],
+ y: projection(item.coordinates)[1],
+ width: 30,
+ height: 30
+})
+
+export default geoLayout
diff --git a/client/src/layouts/monthLayout.js b/client/src/layouts/monthLayout.js
new file mode 100644
index 0000000000..9ca7035dea
--- /dev/null
+++ b/client/src/layouts/monthLayout.js
@@ -0,0 +1,37 @@
+import { DATE_LAYOUT_FORMAT } from "layouts/utils"
+import moment from "moment"
+const monthLayout = (item, dimensions, viewDate) => {
+ // figure out which month
+ const momentDate = moment(item.date, DATE_LAYOUT_FORMAT)
+
+ if (!viewDate.isSame(momentDate, "month")) {
+ return null
+ }
+ /** Monday-Tuesday ....
+ * [0,0]-[1,0]**********
+ * [0,1]***************
+ * .
+ * .
+ **/
+ // This is the [0,0] day of the month
+ const firstDayofFirstWeekOfTheMonth = moment(momentDate)
+ .startOf("month")
+ .startOf("isoWeek")
+
+ const endOfMonth = moment(momentDate).endOf("month")
+
+ const numOfWeeks = endOfMonth.diff(firstDayofFirstWeekOfTheMonth, "weeks") + 1
+ const daysDiff = momentDate.diff(firstDayofFirstWeekOfTheMonth, "days")
+
+ const weekDiff = Math.floor(daysDiff / 7)
+ const weekDayDiff = daysDiff % 7
+
+ return {
+ x: (dimensions.width * weekDayDiff) / 7,
+ y: (dimensions.height * weekDiff) / numOfWeeks,
+ width: dimensions.width / 7,
+ height: dimensions.height / numOfWeeks
+ }
+}
+
+export default monthLayout
diff --git a/client/src/layouts/useLayout.js b/client/src/layouts/useLayout.js
new file mode 100644
index 0000000000..07acd8f04d
--- /dev/null
+++ b/client/src/layouts/useLayout.js
@@ -0,0 +1,25 @@
+import LAYOUTS from "layouts"
+import { INIT_LAYOUT_STATES, LAYOUT_HEADERS } from "layouts/utils"
+import { useMemo } from "react"
+import useDimensions from "react-use-dimensions"
+
+const useLayout = layoutType => {
+ const [ref, dimensions] = useDimensions()
+ const vars = useMemo(() => {
+ const chartHeader = LAYOUT_HEADERS[layoutType]
+ const specificLayout = LAYOUTS[layoutType]
+ const initViewState = INIT_LAYOUT_STATES[layoutType]
+ // we will cal the layout with item and args related to that layout (e.g selectedDate for date layouts, map location for geo layout)
+ const layout = (item, viewArgs) => {
+ return !dimensions?.width || !dimensions?.height
+ ? null
+ : specificLayout(item, dimensions, viewArgs)
+ }
+
+ return [chartHeader, layout, initViewState]
+ }, [layoutType, dimensions])
+
+ return [...vars, ref]
+}
+
+export default useLayout
diff --git a/client/src/layouts/utils.js b/client/src/layouts/utils.js
new file mode 100644
index 0000000000..75d8aa0991
--- /dev/null
+++ b/client/src/layouts/utils.js
@@ -0,0 +1,73 @@
+import * as LayoutHeaders from "components/LayoutHeader"
+import _groupBy from "lodash/groupBy"
+import moment from "moment"
+import * as d3 from "d3"
+
+export const LAYOUT_TYPES = {
+ YEAR: "year",
+ MONTH: "month",
+ GEO: "geo"
+}
+
+export const DATE_LAYOUT_FORMAT = "DD-MM-YYYY"
+
+export const LAYOUT_AGGREGATORS = {
+ [LAYOUT_TYPES.GEO]: groupByLocation,
+ [LAYOUT_TYPES.MONTH]: groupByDay,
+ [LAYOUT_TYPES.YEAR]: groupByDay
+}
+
+export const INIT_LAYOUT_STATES = {
+ // FIXME: Add default location when ready
+ [LAYOUT_TYPES.GEO]: {},
+ [LAYOUT_TYPES.MONTH]: moment(),
+ [LAYOUT_TYPES.YEAR]: moment()
+}
+
+export const LAYOUT_HEADERS = {
+ [LAYOUT_TYPES.GEO]: LayoutHeaders.GeoHeader,
+ [LAYOUT_TYPES.MONTH]: LayoutHeaders.MonthHeader,
+ [LAYOUT_TYPES.YEAR]: LayoutHeaders.YearHeader
+}
+
+export function groupByDay(inItems) {
+ const aggregationKey = "date"
+ const outItems = []
+ // group items by same day in an object { day1: [item1, item2], day2: [item3, item4]}
+ const tempItemsObj = _groupBy(inItems, item =>
+ moment(item[aggregationKey]).format(DATE_LAYOUT_FORMAT)
+ )
+
+ // aggregate from that object to a list of objects
+ Object.keys(tempItemsObj).forEach(dayStr => {
+ outItems.push({
+ aggregationKey,
+ [aggregationKey]: dayStr,
+ numOfEvents: tempItemsObj[dayStr].length
+ })
+ })
+
+ return [outItems, aggregationKey]
+}
+
+export function groupByLocation(inItems) {
+ const reducer = (clusters, currentItem) => {
+ for (const cluster of clusters) {
+ for (const clusterItem of cluster.items) {
+ if (
+ d3.geoDistance(clusterItem.coordinates, currentItem.coordinates) <
+ 0.01
+ ) {
+ cluster.items.push(currentItem)
+ return clusters
+ }
+ }
+ }
+ clusters.push({
+ coordinates: currentItem.coordinates,
+ items: [currentItem]
+ })
+ return clusters
+ }
+ return [inItems.reduce(reducer, []), "coordinates"]
+}
diff --git a/client/src/layouts/yearLayout.js b/client/src/layouts/yearLayout.js
new file mode 100644
index 0000000000..2b5d9952db
--- /dev/null
+++ b/client/src/layouts/yearLayout.js
@@ -0,0 +1,40 @@
+import { DATE_LAYOUT_FORMAT } from "layouts/utils"
+import moment from "moment"
+
+const yearLayout = (item, dimensions, viewDate) => {
+ // figure out which year input is
+ const momentDate = moment(item.date, DATE_LAYOUT_FORMAT)
+ if (!viewDate.isSame(momentDate, "year")) {
+ return null
+ }
+ // figure out where the item located according to its day
+ // calculate how much x-y translation needed
+ /** Jan Feb ....
+ * Monday [0,0][1,0]**********
+ * Tuesday [0,1]***************
+ * .
+ * .
+ **/
+ // First day of first week of the year is basically [0,0] coordinates of the chart
+ const firstDayOfFirstWeekofTheYear = moment(momentDate)
+ .startOf("year")
+ .startOf("isoWeek")
+
+ const endOfYear = moment(momentDate).endOf("year")
+
+ const numOfWeeks = endOfYear.diff(firstDayOfFirstWeekofTheYear, "weeks") + 1
+ const dayDiff = momentDate.diff(firstDayOfFirstWeekofTheYear, "days")
+
+ const weekDiff = Math.floor(dayDiff / 7)
+
+ const weekDayDiff = dayDiff % 7
+
+ return {
+ x: (dimensions.width * weekDiff) / numOfWeeks,
+ y: (dimensions.height * weekDayDiff) / 7,
+ width: dimensions.width / numOfWeeks,
+ height: dimensions.height / 7
+ }
+}
+
+export default yearLayout
diff --git a/client/stories/Button.stories.js b/client/stories/Button.stories.js
index 2854110cae..c871c58ee9 100644
--- a/client/stories/Button.stories.js
+++ b/client/stories/Button.stories.js
@@ -1,9 +1,8 @@
import React from "react"
-
import { Button } from "./Button"
export default {
- title: "Example/Button",
+ title: "Storybook/Examples/Button",
component: Button,
argTypes: {
backgroundColor: { control: "color" }
diff --git a/client/stories/GeneralBanner.stories.js b/client/stories/GeneralBanner.stories.js
index fd046ba913..5e2632bddd 100644
--- a/client/stories/GeneralBanner.stories.js
+++ b/client/stories/GeneralBanner.stories.js
@@ -1,5 +1,5 @@
+import GeneralBanner from "components/GeneralBanner"
import React from "react"
-import GeneralBanner from "../src/components/GeneralBanner"
export default {
title: "ANET/GeneralBanner",
diff --git a/client/stories/Header.stories.js b/client/stories/Header.stories.js
index 37c563fc2b..06bcb1fc0f 100644
--- a/client/stories/Header.stories.js
+++ b/client/stories/Header.stories.js
@@ -1,9 +1,8 @@
import React from "react"
-
import { Header } from "./Header"
export default {
- title: "Example/Header",
+ title: "Storybook/Examples/Header",
component: Header
}
diff --git a/client/stories/HeatMap.stories.js b/client/stories/HeatMap.stories.js
new file mode 100644
index 0000000000..2623f62205
--- /dev/null
+++ b/client/stories/HeatMap.stories.js
@@ -0,0 +1,63 @@
+import HeatMap from "components/HeatMap"
+import moment from "moment"
+import React from "react"
+
+// import faker from "faker"
+
+export default {
+ title: "ANET/HeatMap",
+ component: HeatMap
+}
+
+const containerStyle = {
+ float: "right",
+ overflow: "hidden",
+ width: "100%",
+ minWidth: "700px", // week 7days, at least 100px width for a day
+ height: "350px", // week 7days, at least 50px height for a day,
+ outline: "2px solid red"
+}
+
+const defaultItems = generateMockData(400)
+
+const defaultArgs = {
+ items: defaultItems,
+ style: containerStyle
+}
+
+const Template = args =>
+// FIXME: Add when ready
+// export const Geo = Template.bind({})
+
+// Geo.args = {
+// ...defaultArgs,
+// layoutType: LAYOUT_TYPES.GEO
+// }
+
+export const HEATMAP = Template.bind({})
+
+HEATMAP.args = {
+ ...defaultArgs
+}
+
+function generateMockData(numOfItems) {
+ const items = []
+ const plusMinusDaysInterval = 0.5 * 366
+ const maxVal = plusMinusDaysInterval
+ const minVal = -maxVal
+
+ for (let i = 0; i < numOfItems; i++) {
+ const randomNumOfEvents = Math.floor(Math.random() * 10)
+ const newItem = {
+ date: moment().add(
+ Math.floor(Math.random() * (maxVal - minVal) + minVal),
+ "days"
+ ),
+ coordinates: [45 + Math.random() * 10, 5 + Math.random() * 10],
+ numOfEvents: randomNumOfEvents
+ }
+ newItem.id = newItem.date.format()
+ items.push(newItem)
+ }
+ return items
+}
diff --git a/client/stories/Introduction.stories.mdx b/client/stories/Introduction.stories.mdx
index cdf16c59c7..ee02199888 100644
--- a/client/stories/Introduction.stories.mdx
+++ b/client/stories/Introduction.stories.mdx
@@ -8,7 +8,7 @@ import Plugin from './assets/plugin.svg';
import Repo from './assets/repo.svg';
import StackAlt from './assets/stackalt.svg';
-
+
# Welcome to Storybook
diff --git a/client/stories/Page.stories.js b/client/stories/Page.stories.js
index 4952a48c19..8caf526c4d 100644
--- a/client/stories/Page.stories.js
+++ b/client/stories/Page.stories.js
@@ -1,10 +1,9 @@
import React from "react"
-
-import { Page } from "./Page"
import * as HeaderStories from "./Header.stories"
+import { Page } from "./Page"
export default {
- title: "Example/Page",
+ title: "Storybook/Examples/Page",
component: Page
}
diff --git a/client/stories/RichTextEditor.stories.js b/client/stories/RichTextEditor.stories.js
index 56323b59b0..afdd8c2470 100644
--- a/client/stories/RichTextEditor.stories.js
+++ b/client/stories/RichTextEditor.stories.js
@@ -1,5 +1,5 @@
+import RichTextEditor from "components/RichTextEditor"
import React from "react"
-import RichTextEditor from "../src/components/RichTextEditor"
export default {
title: "ANET/RichTextEditor",