-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create custom heat-map components, layouts, aggregation widgets #3196
base: main
Are you sure you want to change the base?
Changes from all commits
5afb8c5
a4f8435
e70df8a
6c97440
b0ee7fe
119691e
ef973f5
dfcaed5
c51c935
328b592
66cc367
d0a7e1a
cc21390
b80a227
a1391c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<HeaderElement viewState={viewState} setViewState={setViewState} /> | ||
<svg ref={ref} style={style}> | ||
{aggregatedItems.map(item => { | ||
const boundingRect = layout(item, viewState) | ||
// if it isn't in the layout ( e.g different year, month) | ||
if (!boundingRect) { | ||
return null | ||
} | ||
return ( | ||
<g | ||
transform={`translate(${boundingRect.x}, ${boundingRect.y})`} | ||
key={JSON.stringify(item)} | ||
> | ||
<Widget | ||
item={item} | ||
dimensions={boundingRect} | ||
widgetConfig={widgetConfig} | ||
/> | ||
</g> | ||
) | ||
})} | ||
</svg> | ||
</> | ||
) | ||
} | ||
Chart.propTypes = { | ||
items: PropTypes.arrayOf(PropTypes.object), | ||
layoutType: PropTypes.string, | ||
widgetElement: PropTypes.func, | ||
style: PropTypes.object, | ||
widgetConfig: PropTypes.object | ||
} | ||
export default Chart |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the mapping of items to widgets should is not specific to dates, It should probably happen in Chart, as it is the main function of a Chart. |
||
const boundingRect = layout(item, viewDate) | ||
// if it isn't in the layout ( e.g different year, month) | ||
if (!boundingRect) { | ||
return null | ||
} | ||
return ( | ||
<g | ||
transform={`translate(${boundingRect.x}, ${boundingRect.y})`} | ||
key={item.date} | ||
> | ||
<Widget | ||
item={item} | ||
dimensions={boundingRect} | ||
widgetConfig={widgetConfig} | ||
/> | ||
</g> | ||
) | ||
})} | ||
</> | ||
) | ||
} | ||
DateChart.propTypes = { | ||
items: PropTypes.arrayOf(PropTypes.object), | ||
layout: PropTypes.func, | ||
widgetElement: PropTypes.func, | ||
viewState: PropTypes.object, | ||
widgetConfig: PropTypes.object | ||
} | ||
|
||
export default DateChart |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div> | ||
<div> | ||
<label htmlFor="layouts">Layout:</label> | ||
<select | ||
value={layout} | ||
name="layouts" | ||
id="layouts" | ||
onChange={e => setLayout(e.target.value)} | ||
> | ||
<option value={LAYOUT_TYPES.MONTH}>Month</option> | ||
<option value={LAYOUT_TYPES.YEAR}>Year</option> | ||
<option value={LAYOUT_TYPES.GEO}>Geo</option> | ||
</select> | ||
</div> | ||
<Chart | ||
items={items} | ||
layoutType={layout} | ||
widgetElement={HeatWidget} | ||
style={style} | ||
widgetConfig={heatConfig} | ||
/> | ||
</div> | ||
) | ||
} | ||
HeatMap.propTypes = { | ||
items: PropTypes.arrayOf(PropTypes.object), | ||
style: PropTypes.object | ||
} | ||
|
||
export default HeatMap |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<header | ||
style={{ | ||
display: "flex", | ||
flexDirection: "row", | ||
justifyContent: "space-between", | ||
alignItems: "center", | ||
margin: "10px auto" | ||
}} | ||
> | ||
<h2>{viewDate.format(format)}</h2> | ||
<div> | ||
<Button | ||
onClick={() => setViewDate(date => moment(date).add(-1, dateScale))} | ||
> | ||
Prev | ||
</Button> | ||
<Button onClick={() => setViewDate(moment())}>Today</Button> | ||
<Button | ||
onClick={() => setViewDate(date => moment(date).add(1, dateScale))} | ||
> | ||
Next | ||
</Button> | ||
</div> | ||
</header> | ||
) | ||
} | ||
|
||
DateHeader.propTypes = { | ||
viewDate: PropTypes.object, | ||
setViewDate: PropTypes.func, | ||
dateScale: PropTypes.string, | ||
format: PropTypes.string | ||
} | ||
|
||
export const YearHeader = ({ viewState, setViewState }) => { | ||
return ( | ||
<DateHeader | ||
viewDate={viewState} | ||
setViewDate={setViewState} | ||
dateScale="years" | ||
format="YYYY" | ||
/> | ||
) | ||
} | ||
|
||
YearHeader.propTypes = { | ||
viewState: PropTypes.object, | ||
setViewState: PropTypes.func | ||
} | ||
|
||
export const MonthHeader = ({ viewState, setViewState }) => { | ||
return ( | ||
<DateHeader | ||
viewDate={viewState} | ||
setViewDate={setViewState} | ||
dateScale="months" | ||
format="MMMM-YYYY" | ||
/> | ||
) | ||
} | ||
|
||
MonthHeader.propTypes = { | ||
viewState: PropTypes.object, | ||
setViewState: PropTypes.func | ||
} | ||
|
||
// FIXME: fix when geolayout ready | ||
export const GeoHeader = ({ | ||
viewState: viewLocation, | ||
setViewState: setViewLocation | ||
}) => { | ||
console.log(setViewLocation) | ||
|
||
return <header /> | ||
} | ||
|
||
GeoHeader.propTypes = { | ||
viewState: PropTypes.object, | ||
setViewState: PropTypes.func | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<> | ||
<rect | ||
width={dimensions.width} | ||
height={dimensions.height} | ||
stroke="purple" | ||
strokeWidth="2" | ||
fill={bgc} | ||
/> | ||
<text | ||
x={dimensions.width / 2} | ||
y={dimensions.height / 2} | ||
dominantBaseline="middle" | ||
fontSize="16" | ||
fill={heatConfig.textColor} | ||
textAnchor="middle" | ||
> | ||
{item.numOfEvents} | ||
</text> | ||
</> | ||
) | ||
} | ||
HeatWidget.propTypes = { | ||
item: PropTypes.object, | ||
dimensions: PropTypes.object, | ||
widgetConfig: PropTypes.object | ||
} | ||
export default HeatWidget |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -220,3 +220,30 @@ export function reportsToEvents(reports) { | |
} | ||
}) | ||
} | ||
export const COLOR_NAMES_TO_RGB = { | ||
red: "rgb(155, 0, 0, ", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could probably make use of https://github.com/d3/d3-color |
||
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)` | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A chart should not assume it contains aggregations. It could be a simple bar chart.
It should take a collection of items, and map them to the appropriate GUI components based on how it is configured.
I suggest to have chart take a collection of items. If they have been aggregated, it should take as an input the list of aggregated groupings as items, with the original items referenced from the aggregations