mirror of
https://github.com/heavyai/heavyai-charting.git
synced 2026-01-25 14:57:45 +00:00
* A bit better generalization of the lasso tools by adding a lasso tool type identifier to each tool subclass. * Add some new events for when lasso tool buttons are activated or deactivated. This may be useful to check the case that a cross section line was deleted as a result of the user drawing a new line. * Adds a more general-purpose fireEvent utility method to the base ShapeHandler class that will auto-tag the event with the current lasso_tool_type. This allows for a more preferred way to check whether a created shape is a result of a specific tool or not. * Tags lasso tool shapes with the tool type which may be useful in callback handlers * Enforces the restriction that only one cross-section line can be drawn at a time. It does this by deleting any previously existing line shapes on a new "create" click. * Adds an emum that adds more flexibility as to when a ShapeHandler's destroy() method might be called so derived classes can do more context-aware destroys, i.e. all shapes being destroyed as a result of a "cancel", or just the temporary shapes being destroyed after a shape is successfully completed. * Fill out the remaining pieces of throwing a DRAW STOPPED event signal with a reason (cancelled, completed, reset, or deactivated)
464 lines
16 KiB
JavaScript
464 lines
16 KiB
JavaScript
import * as HeavyCharting from "../src/index.js"
|
|
import * as HeavyConnect from "@heavyai/connector"
|
|
import * as HeavyCrossfilter from "@heavyai/crossfilter"
|
|
import mapboxgl from "mapbox-gl"
|
|
import _ from "lodash"
|
|
import LatLonPolyLine from "../src/mixins/ui/lasso-shapes/LatLonPolyLine.js"
|
|
import * as LatLonUtils from "../src/utils/utils-latlon"
|
|
import {
|
|
LassoShapeEventConstants,
|
|
LassoGlobalEventConstants
|
|
} from "../src/mixins/ui/lasso-event-constants"
|
|
import LassoToolSetTypes from "../src/mixins/ui/lasso-tool-set-types.js"
|
|
import { Point2d } from "@heavyai/draw/dist/draw"
|
|
import assert from "assert"
|
|
|
|
function create_charts(
|
|
linemap_crossfilter,
|
|
crosssection_crossfilter,
|
|
connection
|
|
) {
|
|
const countGroup = linemap_crossfilter.groupAll()
|
|
HeavyCharting.countWidget(".data-count")
|
|
.dimension(linemap_crossfilter)
|
|
.group(countGroup)
|
|
|
|
/* ----------------BACKEND RENDERED POINT MAP WITH LASSO TOOL EXAMPLE----------------------- */
|
|
|
|
linemap_crossfilter
|
|
.dimension(null)
|
|
.projectOn(["longitude", "latitude", "Wind_Speed"])
|
|
|
|
linemap_crossfilter.dimension("model_ts").filter("2022-09-25 18:00:00")
|
|
linemap_crossfilter.dimension("forecast_hour").filter(0)
|
|
linemap_crossfilter.dimension("isobaric_level").filter(1000)
|
|
|
|
const map_parent = document.getElementById("crosssection-map")
|
|
|
|
const map_width = map_parent.clientWidth - 30
|
|
const map_height =
|
|
Math.max(map_parent.clientHeight, window.innerHeight || 0) - 100
|
|
|
|
const mapboxToken =
|
|
"pk.eyJ1IjoibWFwZCIsImEiOiJjaWV1a3NqanYwajVsbmdtMDZzc2pneDVpIn0.cJnk8c2AxdNiRNZWtx5A9g"
|
|
|
|
let cross_section_layer = null
|
|
const cross_section_layer_state = {
|
|
transform: [
|
|
{
|
|
crossSection2d: {
|
|
x: "longitude",
|
|
y: "latitude",
|
|
z: "isobaric_level",
|
|
crossSectionLine: [
|
|
[-122.875, 33],
|
|
[-113.125, 33]
|
|
],
|
|
crossSectionDimensionName: "distance"
|
|
}
|
|
}
|
|
],
|
|
mark: { type: "mesh2d" },
|
|
encoding: {
|
|
x: {
|
|
field: "distance",
|
|
label: "distance"
|
|
},
|
|
y: {
|
|
field: "isobaric_level",
|
|
label: "isobaric_level"
|
|
},
|
|
color: {
|
|
field: "Wind_Speed",
|
|
type: "quantitative",
|
|
scale: {
|
|
range: ["blue", "red"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const pointMapChart = HeavyCharting.rasterChart(
|
|
map_parent,
|
|
true,
|
|
null,
|
|
mapboxgl
|
|
)
|
|
.con(connection)
|
|
.width(map_width)
|
|
.height(map_height)
|
|
.mapUpdateInterval(750)
|
|
.mapStyle("json/dark-v8.json")
|
|
.mapboxToken(mapboxToken) // need a mapbox accessToken for loading the tiles
|
|
.popupSearchRadius(2)
|
|
.useGeoTypes(true)
|
|
|
|
const raster_mesh2d_layer = HeavyCharting.rasterLayer("mesh2d")
|
|
.crossfilter(linemap_crossfilter)
|
|
.setState({
|
|
transform: [
|
|
{
|
|
rasterMesh2d: {}
|
|
}
|
|
],
|
|
mark: { type: "mesh2d" },
|
|
encoding: {
|
|
longitude: {
|
|
field: "longitude",
|
|
label: "longitude"
|
|
},
|
|
latitude: {
|
|
field: "latitude",
|
|
label: "latitude"
|
|
},
|
|
color: {
|
|
field: "Air_Temperature",
|
|
type: "quantitative",
|
|
scale: {
|
|
range: ["blue", "red"]
|
|
}
|
|
}
|
|
}
|
|
})
|
|
// .xDim(x_dimension)
|
|
// .yDim(y_dimension)
|
|
|
|
pointMapChart
|
|
.pushLayer("raster_mesh", raster_mesh2d_layer)
|
|
.init()
|
|
.then(() => {
|
|
/* --------------------------LASSO TOOL DRAW CONTROL------------------------------ */
|
|
pointMapChart.addDrawControl(LassoToolSetTypes.kCrossSection)
|
|
|
|
/**
|
|
* This is a function to be called from shape create/edit callback method
|
|
* that will set the new cross-section state from the current drawn line.
|
|
* @param {Draw.BaseShape} shape
|
|
* @returns {Boolean} Returns true if the cross-section state was modified,
|
|
* false otherwise
|
|
*/
|
|
function setCrossSectionLineStateFromShape(shape) {
|
|
if (shape instanceof LatLonPolyLine) {
|
|
const verts = shape.vertsRef
|
|
|
|
// various asserts to ensure we have appropriate/expected input/output
|
|
// data. The line drawn, for example, must currently only have two vertices.
|
|
// The current state of the the cross section layer must have a crossSection2d
|
|
// transform
|
|
assert(verts.length === 2)
|
|
assert(cross_section_layer_state)
|
|
assert(Array.isArray(cross_section_layer_state.transform))
|
|
assert(cross_section_layer_state.transform.length === 1)
|
|
assert(cross_section_layer_state.transform[0].crossSection2d)
|
|
assert(
|
|
Array.isArray(
|
|
cross_section_layer_state.transform[0].crossSection2d
|
|
.crossSectionLine
|
|
)
|
|
)
|
|
assert(
|
|
cross_section_layer_state.transform[0].crossSection2d
|
|
.crossSectionLine.length === 2
|
|
)
|
|
// eslint-disable-next-line max-nested-callbacks
|
|
verts.forEach((vert, i) => {
|
|
// get the corresponding vertex definition from the
|
|
// cross section state to copy the vertex data to.
|
|
const vert_copy =
|
|
cross_section_layer_state.transform[0].crossSection2d
|
|
.crossSectionLine[i]
|
|
|
|
// copy the vert
|
|
Point2d.copy(vert_copy, vert)
|
|
|
|
// the vert by default is in object-local space, i.e. no affine transforms
|
|
// like scale/rotate are applied yet. So apply the transforms here.
|
|
Point2d.transformMat2d(vert_copy, vert_copy, shape.globalXform)
|
|
|
|
// since mapd-draw currently only draws points in a cartesian space, the stored
|
|
// verts are therefore in mercator coordinates, so convert from mercator to
|
|
// wgs84 (srid 4326) lat/lon
|
|
LatLonUtils.conv900913To4326(vert_copy, vert_copy)
|
|
})
|
|
|
|
// now reset the state for the cross-section layer
|
|
// NOTE: technically we don't need to call setState() here as the
|
|
// state is currently stored as a reference at the raster layer level,
|
|
// but this it is safer to push the state when done modifying it in the
|
|
// event that it is deep-copied
|
|
cross_section_layer.setState(cross_section_layer_state)
|
|
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Callback function called when a cross section line is edited.
|
|
* @param {Object} event_obj Event object describing the context
|
|
* of event fired. In the case of a cross-section line
|
|
* edit, this will be an event object describing a
|
|
* LassoShapeEventConstants.LASSO_SHAPE_EDIT_END event.
|
|
* What we care about here is event_obj.target, which
|
|
* is the cross section line instance
|
|
*/
|
|
const shape_edit_end_callback = event_obj => {
|
|
assert(event_obj.target instanceof LatLonPolyLine)
|
|
if (setCrossSectionLineStateFromShape(event_obj.target)) {
|
|
HeavyCharting.renderAllAsync()
|
|
}
|
|
}
|
|
|
|
let is_cross_section_enabled = false
|
|
pointMapChart.onDrawEvent(
|
|
LassoGlobalEventConstants.LASSO_TOOL_TYPE_ACTIVATED,
|
|
event_obj => {
|
|
if (event_obj.lasso_tool_type === LassoToolSetTypes.kCrossSection) {
|
|
is_cross_section_enabled = true
|
|
}
|
|
}
|
|
)
|
|
|
|
pointMapChart.onDrawEvent(
|
|
LassoGlobalEventConstants.LASSO_TOOL_TYPE_DEACTIVATED,
|
|
event_obj => {
|
|
if (event_obj.lasso_tool_type === LassoToolSetTypes.kCrossSection) {
|
|
is_cross_section_enabled = false
|
|
}
|
|
}
|
|
)
|
|
|
|
// setup an event trigger for when a lasso shape is created
|
|
pointMapChart.onDrawEvent(
|
|
LassoGlobalEventConstants.LASSO_SHAPE_CREATE,
|
|
event_obj => {
|
|
if (cross_section_layer) {
|
|
const { shape, lasso_tool_type = null } = event_obj
|
|
if (lasso_tool_type === LassoToolSetTypes.kCrossSection) {
|
|
// if the newly created shape was created as a result of the
|
|
// CrossSection tool then attach an edit callback to it to
|
|
// capture any/all edits after creation.
|
|
|
|
// NOTE: adding an edit event callback directly on a cross-section line shape is one
|
|
// way to do this. An alternative way is to add a global edit event callback for all edited
|
|
// shapes. See the commented out code w/ note below.
|
|
// The advantage of doing it this way directly on the shape is it is potentially more performant
|
|
// as you don't have to iterate the edited shapes in the list to find the cross section
|
|
// line. You know right away that it is a cross section line.
|
|
// The disadvantage is you may need to disable the callback when the shape is deleted.
|
|
// See other note in the LASSO_SHAPE_DESTROY event callback below
|
|
shape.on(
|
|
LassoShapeEventConstants.LASSO_SHAPE_EDIT_END,
|
|
shape_edit_end_callback
|
|
)
|
|
|
|
// now update the cross section state after shape creation.
|
|
if (setCrossSectionLineStateFromShape(shape)) {
|
|
HeavyCharting.renderAllAsync()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
// NOTE: this is an alternative way to capture shape edit events globally
|
|
// rather than locally. This way you need to check the list of shapes for
|
|
// the cross-section line shape, tho generally speaking the list is going
|
|
// to be very small
|
|
// pointMapChart.onDrawEvent(
|
|
// LassoGlobalEventConstants.LASSO_SHAPE_EDITS_END,
|
|
// event_obj => {
|
|
// if (cross_section_layer) {
|
|
// event_obj.shapes.forEach(shape => {
|
|
// if (shape instanceof LatLonPolyLine) {
|
|
// // now update the cross section state after shape creation.
|
|
// if (setCrossSectionLineStateFromShape(shape)) {
|
|
// HeavyCharting.renderAllAsync()
|
|
// }
|
|
// }
|
|
// })
|
|
// }
|
|
// }
|
|
// )
|
|
|
|
// capture the shape delete event and deactivate the edit callback on
|
|
// the shape if it is a cross-section line
|
|
pointMapChart.onDrawEvent(
|
|
LassoGlobalEventConstants.LASSO_SHAPE_DESTROY,
|
|
event_obj => {
|
|
const { shape } = event_obj
|
|
if (shape instanceof LatLonPolyLine) {
|
|
// may want to reset the cross-section line to something else
|
|
// here when the original drawn cross section line is deleted
|
|
// For example, one approach is to set the line to be the bottom
|
|
// edge of the current map view or the bottom edge of the raster
|
|
// volume
|
|
// Tho you may want to ignore doing this if the shape was deleted
|
|
// as a result of a new line being drawn. You can check for this
|
|
// case by checking the "is_cross_section_enabled" variable that's
|
|
// tied to tool activate/deactivate signals
|
|
if (!is_cross_section_enabled) {
|
|
// no cross-section lines are currently drawn or being drawn on
|
|
// the map, so may want to reset the cross section endpoints to
|
|
// some default here.
|
|
}
|
|
|
|
// turn off the croos-section line-local edit event callback
|
|
// this is not really necessary, but it's good shutdown/cleanup
|
|
// practice
|
|
// NOTE: this is not necessary at all if capturing the shape
|
|
// edit event globally, and not locally, via the
|
|
// LassoGlobalEventConstants.LASSO_SHAPE_EDITS_END event signal
|
|
shape.off(
|
|
LassoShapeEventConstants.LASSO_SHAPE_EDIT_END,
|
|
shape_edit_end_callback
|
|
)
|
|
}
|
|
}
|
|
)
|
|
|
|
HeavyCharting.renderAllAsync()
|
|
})
|
|
|
|
crosssection_crossfilter
|
|
.dimension(null)
|
|
.projectOn(["longitude", "latitude", "isobaric_level", "Wind_Speed"])
|
|
|
|
crosssection_crossfilter.dimension("model_ts").filter("2022-09-25 18:00:00")
|
|
crosssection_crossfilter.dimension("forecast_hour").filter(0)
|
|
|
|
const crosssection_parent = document.getElementById("crosssection-chart")
|
|
const crosssection_width = crosssection_parent.clientWidth - 30
|
|
const crosssection_height =
|
|
Math.max(crosssection_parent.clientHeight, window.innerHeight || 0) - 100
|
|
|
|
/*
|
|
* We need the min/max of the z dimension of the raster volume to
|
|
* initialize a proper view. We calculate the extents first and then
|
|
* build the cross section view.
|
|
*/
|
|
const extentMeasures = [
|
|
{
|
|
expression: "isobaric_level",
|
|
agg_mode: "min",
|
|
name: "ymin"
|
|
},
|
|
{
|
|
expression: "isobaric_level",
|
|
agg_mode: "max",
|
|
name: "ymax"
|
|
}
|
|
]
|
|
|
|
crosssection_crossfilter
|
|
.groupAll()
|
|
.reduce(extentMeasures)
|
|
.valuesAsync(true)
|
|
.then(extents => {
|
|
cross_section_layer = HeavyCharting.rasterLayer("mesh2d")
|
|
.crossfilter(crosssection_crossfilter)
|
|
.setState(cross_section_layer_state)
|
|
// by default, the dimension along the length of the line is a
|
|
// normalized distance where 0 is the start of the line and 1
|
|
// is the end of the line
|
|
.xDim([0, 1])
|
|
|
|
// the isobaric_level column has a range of 0-1000
|
|
// where 0 is the upper-most level of the atmosphere
|
|
// and 1000 is at the surface. As such, reverse the
|
|
// min/max here so the chart reads from surface to
|
|
// high-atmosphere when going up in Y
|
|
.yDim([extents.ymax, extents.ymin])
|
|
|
|
const cross_section_chart = HeavyCharting.rasterChart(
|
|
crosssection_parent,
|
|
false
|
|
)
|
|
.con(connection)
|
|
.width(crosssection_width)
|
|
.height(crosssection_height)
|
|
|
|
// render the grid lines
|
|
.renderHorizontalGridLines(true)
|
|
.renderVerticalGridLines(true)
|
|
|
|
// set the axis labels
|
|
.xAxisLabel("X Axis")
|
|
.yAxisLabel("Y Axis")
|
|
|
|
.enableInteractions(true)
|
|
|
|
cross_section_chart
|
|
.pushLayer("cross_section", cross_section_layer)
|
|
.init()
|
|
.then(() => {
|
|
HeavyCharting.renderAllAsync()
|
|
})
|
|
|
|
/**
|
|
* Setup resize event
|
|
*/
|
|
|
|
/* Here we listen to any resizes of the main window. On resize we resize the corresponding widgets and call dc.renderAll() to refresh everything */
|
|
window.addEventListener("resize", _.debounce(resize_all, 500))
|
|
|
|
function resize_all() {
|
|
const map_width = map_parent.clientWidth - 30
|
|
const map_height =
|
|
Math.max(map_parent.clientHeight, window.innerHeight || 0) - 200
|
|
|
|
const crosssection_width = crosssection_parent.clientWidth - 30
|
|
const crosssection_height =
|
|
Math.max(crosssection_parent.clientHeight, window.innerHeight || 0) -
|
|
100
|
|
|
|
pointMapChart.map().resize()
|
|
pointMapChart.isNodeAnimate = false
|
|
pointMapChart
|
|
.width(map_width)
|
|
.height(map_height)
|
|
.render()
|
|
|
|
cross_section_chart.map().resize()
|
|
cross_section_chart.isNodeAnimate = false
|
|
cross_section_chart
|
|
.width(crosssection_width)
|
|
.height(crosssection_height)
|
|
.render()
|
|
|
|
HeavyCharting.redrawAllAsync()
|
|
}
|
|
})
|
|
}
|
|
|
|
function init() {
|
|
const hostname = "10.2.1.12"
|
|
const dbName = "heavyai"
|
|
const user = "admin"
|
|
const password = "HyperInteractive"
|
|
new HeavyConnect.DbCon()
|
|
.protocol("http")
|
|
.host(hostname)
|
|
.port("6278")
|
|
.dbName(dbName)
|
|
.user(user)
|
|
.password(password)
|
|
.connect((error, con) => {
|
|
if (error) {
|
|
throw error
|
|
}
|
|
const table_name = "noaa_gfs_isobaric_canvaz"
|
|
HeavyCrossfilter.crossfilter(con, table_name).then(
|
|
linemap_crossfilter => {
|
|
HeavyCrossfilter.crossfilter(con, table_name).then(
|
|
crosssection_crossfilter => {
|
|
create_charts(linemap_crossfilter, crosssection_crossfilter, con)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init, false)
|