diff --git a/react/src/demo/example-data/welllayer_continuous_template.json b/react/src/demo/example-data/welllayer_continuous_template.json new file mode 100644 index 000000000..37fc68b75 --- /dev/null +++ b/react/src/demo/example-data/welllayer_continuous_template.json @@ -0,0 +1,49 @@ +[ + { + "name": "Continuous Template", + "properties": [ + { + "objectName" : "PORO", + "colorTable" : "Rainbow", + "context" : "wells-layer", + "scale": "linear" + }, + { + "objectName" : "PERM", + "colorTable" : "Physics", + "context" : "wells-layer", + "scale": "linear" + }, + { + "objectName" : "MD", + "colorTable" : "Physics reverse", + "context" : "wells-layer", + "scale": "linear" + }, + { + "objectName" : "PORO_TOT", + "colorTable" : "Rainbow reverse", + "context" : "wells-layer", + "scale": "linear" + }, + { + "objectName" : "NTG", + "colorTable" : "Rainbow", + "context" : "wells-layer", + "scale": "linear" + }, + { + "objectName" : "PERM_TOT", + "colorTable" : "Physics", + "context" : "wells-layer", + "scale": "linear" + }, + { + "objectName" : "SW", + "colorTable" : "Rainbow", + "context" : "wells-layer", + "scale": "linear" + } + ] + } +] \ No newline at end of file diff --git a/react/src/lib/components/DeckGLMap/DeckGLMap.jsx b/react/src/lib/components/DeckGLMap/DeckGLMap.jsx index 080c50fcc..352495dc8 100644 --- a/react/src/lib/components/DeckGLMap/DeckGLMap.jsx +++ b/react/src/lib/components/DeckGLMap/DeckGLMap.jsx @@ -186,6 +186,14 @@ DeckGLMap.propTypes = { * Prop containing edited data from layers */ editedData: PropTypes.object, + /** + * Prop containing color table data + */ + colorTables: PropTypes.arrayOf(PropTypes.object), + /** + * Prop containing color template data + */ + colorTemplate: PropTypes.arrayOf(PropTypes.object), }; export default DeckGLMap; diff --git a/react/src/lib/components/DeckGLMap/components/ContinuousLegend.tsx b/react/src/lib/components/DeckGLMap/components/ContinuousLegend.tsx index 229a20ce5..af3eafa75 100644 --- a/react/src/lib/components/DeckGLMap/components/ContinuousLegend.tsx +++ b/react/src/lib/components/DeckGLMap/components/ContinuousLegend.tsx @@ -1,19 +1,18 @@ import React from "react"; -import { - select, - scaleLinear, - range, - axisRight, - rgb, - ScaleSequential, -} from "d3"; -import { interpolatorContinuous } from "../utils/continuousLegend"; +import { RGBToHex } from "../utils/continuousLegend"; +import { select, scaleLinear, scaleSequential, axisBottom } from "d3"; interface legendProps { min: number; max: number; dataObjectName: string; position: number[]; + colorTableColors: [number, number, number, number][]; +} + +interface ItemColor { + color: string; + offset: number; } const ContinuousLegend: React.FC = ({ @@ -21,86 +20,77 @@ const ContinuousLegend: React.FC = ({ max, dataObjectName, position, + colorTableColors, }: legendProps) => { - const [legendLoaded, setLegendLoaded] = React.useState(false); React.useEffect(() => { - continuousLegend( - "#legend", - interpolatorContinuous().domain([min, max]) - ); + continuousLegend("#legend"); }, [min, max]); - function continuousLegend( - selected_id: string, - colorscale: ScaleSequential - ) { - const legendheight = 230, - legendwidth = 80, - margin = { top: 15, right: 60, bottom: 15, left: 2 }; + function continuousLegend(selected_id: string) { + const itemColor: ItemColor[] = []; + colorTableColors.forEach((value: [number, number, number, number]) => { + itemColor.push({ + offset: RGBToHex(value).offset, + color: RGBToHex(value).color, + }); + }); - select(selected_id).select("canvas").remove(); select(selected_id).select("svg").remove(); + const colorScale = scaleSequential().domain([min, max]); + // append a defs (for definition) element to your SVG + const svgLegend = select(selected_id).append("svg").attr("width", 300); + const defs = svgLegend.append("defs"); + // append a linearGradient element to the defs and give it a unique id + const linearGradient = defs + .append("linearGradient") + .attr("id", "linear-gradient") + .attr("x1", "0%") + .attr("x2", "100%") //since it's a horizontal linear gradient + .attr("y1", "0%") + .attr("y2", "0%"); + // append multiple color stops by using D3's data/enter step + linearGradient + .selectAll("stop") + .data(itemColor) + .enter() + .append("stop") + .attr("offset", function (data) { + return data.offset + "%"; + }) + .attr("stop-color", function (data) { + return data.color; + }); - const canvas = select(selected_id) - .style("width", 150 + "px") - .append("canvas") - .attr("height", legendheight + 5 - margin.top - margin.bottom) - .attr("width", 1) - .style( - "height", - legendheight + 5 - margin.top - margin.bottom + "px" - ) - .style( - "width", - legendwidth + 13 - margin.left - margin.right + "px" - ) - .style("border", "1px solid") - .node(); - - if (canvas) { - const context = canvas.getContext("2d"); - const legendscale = scaleLinear() - .range([legendheight - margin.top - margin.bottom, 0]) - .domain(colorscale.domain()); + // append title + svgLegend + .append("text") + .attr("class", "legendTitle") + .attr("x", 25) + .attr("y", 20) + .style("text-anchor", "left") + .text(dataObjectName); - if (context) { - const image = context.createImageData(1, legendheight); - range(legendheight).forEach(function (i) { - const c = rgb(colorscale(legendscale.invert(i))); - image.data[4 * i] = c.r; - image.data[4 * i + 1] = c.g; - image.data[4 * i + 2] = c.b; - image.data[4 * i + 3] = 255; - }); - context.putImageData(image, 0, 0); - } + // draw the rectangle and fill with gradient + svgLegend + .append("rect") + .attr("x", 25) + .attr("y", 30) + .attr("width", 250) + .attr("height", 25) + .style("fill", "url(#linear-gradient)"); - const legendaxis = axisRight(legendscale) - .scale(legendscale) - .tickValues(legendscale.domain()); - const svg = select(selected_id) - .append("svg") - .attr("height", legendheight - 3 + "px") - .attr("width", legendwidth - 20 + "px"); + //create tick marks + const xLeg = scaleLinear().domain([min, max]).range([10, 258]); - svg.append("g") - .attr("class", "axis") - .style("font-size", "14px") - .style("font-weight", "700") - .attr( - "transform", - "translate(" + - (80 - margin.left - margin.right - 25) + - "," + - (margin.top + 7) + - ")" - ) - .call(legendaxis) - .selectAll("text") - .style("fill", "#6F6F6F"); + const axisLeg = axisBottom(xLeg).tickValues(colorScale.domain()); - setLegendLoaded(true); - } + svgLegend + .attr("class", "axis") + .append("g") + .attr("transform", "translate(15, 55)") + .style("font-size", "10px") + .style("font-weight", "700") + .call(axisLeg); } return ( @@ -111,9 +101,6 @@ const ContinuousLegend: React.FC = ({ top: position[1], }} > - {legendLoaded && ( - - )}
); diff --git a/react/src/lib/components/DeckGLMap/components/DeckGLWrapper.tsx b/react/src/lib/components/DeckGLMap/components/DeckGLWrapper.tsx index e31b585af..d716b73af 100644 --- a/react/src/lib/components/DeckGLMap/components/DeckGLWrapper.tsx +++ b/react/src/lib/components/DeckGLMap/components/DeckGLWrapper.tsx @@ -13,6 +13,7 @@ import InfoCard from "./InfoCard"; import DistanceScale from "../components/DistanceScale"; import DiscreteColorLegend from "../components/DiscreteLegend"; import ContinuousLegend from "../components/ContinuousLegend"; +import { colorsArray } from "../utils/continuousLegend"; import StatusIndicator from "./StatusIndicator"; import { DrawingLayer, WellsLayer, PieChartLayer } from "../layers"; import { getLogValues, LogCurveDataType } from "../layers/wells/wellsLayer"; @@ -206,6 +207,9 @@ const DeckGLWrapper: React.FC = ({ ); const [isLoaded, setIsLoaded] = React.useState(false); + const [colorsArrays, setcolorsArrays] = React.useState< + [number, number, number, number][] + >([]); const [is3D, setIs3D] = useState(false); @@ -269,6 +273,7 @@ const DeckGLWrapper: React.FC = ({ metadata: { objects: {} }, valueRange: [Math.min(...minArray), Math.max(...maxArray)], }); + setcolorsArrays(colorsArray(wellsLayer?.props?.logName)); } }, [isLoaded, legend, wellsLayer?.props?.logName]); @@ -344,6 +349,7 @@ const DeckGLWrapper: React.FC = ({ max={legendProps.valueRange[1]} dataObjectName={legendProps.title} position={legend.position} + colorTableColors={colorsArrays} /> )} {deckGLLayers && ( diff --git a/react/src/lib/components/DeckGLMap/layers/wells/wellsLayer.ts b/react/src/lib/components/DeckGLMap/layers/wells/wellsLayer.ts index 58cea97e2..fb9c874b7 100644 --- a/react/src/lib/components/DeckGLMap/layers/wells/wellsLayer.ts +++ b/react/src/lib/components/DeckGLMap/layers/wells/wellsLayer.ts @@ -3,9 +3,7 @@ import { ExtendedLayerProps } from "../utils/layerTools"; import { GeoJsonLayer, PathLayer } from "@deck.gl/layers"; import { RGBAColor } from "@deck.gl/core/utils/color"; import { subtract, distance, dot } from "mathjs"; -import { interpolatorContinuous } from "../../utils/continuousLegend"; -import { colorTableData } from "../../components/DiscreteLegend"; -import { color } from "d3-color"; +import { rgbValues } from "../../utils/continuousLegend"; import { Feature, GeometryCollection, @@ -343,8 +341,8 @@ function getLogColor( ): RGBAColor[] { const log_data = getLogValues(d, logrun_name, log_name); const log_info = getLogInfo(d, logrun_name, log_name); - if (log_data.length == 0 || log_info == undefined) return []; + if (log_data.length == 0 || log_info == undefined) return []; const log_color: RGBAColor[] = []; if (log_info.description == "continuous") { const min = Math.min(...log_data); @@ -352,36 +350,24 @@ function getLogColor( const max_delta = max - min; log_data.forEach((value) => { - const rgb = color( - interpolatorContinuous()((value - min) / max_delta) - )?.rgb(); + const rgb = rgbValues(log_name, (value - min) / max_delta); if (rgb != undefined) { - log_color.push([rgb.r, rgb.g, rgb.b]); + if (Array.isArray(rgb)) { + log_color.push([rgb[0], rgb[1], rgb[2]]); + } else { + log_color.push([rgb.r, rgb.g, rgb.b]); + } } }); } else { - const colorsArrayData: [number, number, number, number][] = - colorTableData(log_name); - const log_attributes = getDiscreteLogMetadata(d, log_name)?.objects; - // eslint-disable-next-line - const attributesObject: { [key: string]: any } = {}; - Object.keys(log_attributes).forEach((key) => { - const code = log_attributes[key][1]; - const colorArrays = colorsArrayData.find((value: number[]) => { - return value[0] == code; - }); - if (colorArrays) - attributesObject[key] = [ - [colorArrays[1], colorArrays[2], colorArrays[3]], - code, - ]; - }); log_data.forEach((log_value) => { - const dl_attrs = Object.entries(attributesObject).find( + const dl_attrs = Object.entries(log_attributes).find( ([, value]) => value[1] == log_value )?.[1]; - dl_attrs ? log_color.push(dl_attrs[0]) : log_color.push([0, 0, 0]); + dl_attrs + ? log_color.push(dl_attrs[0]) + : log_color.push([0, 0, 0, 0]); }); } return log_color; diff --git a/react/src/lib/components/DeckGLMap/storybook/DeckGLMap.stories.jsx b/react/src/lib/components/DeckGLMap/storybook/DeckGLMap.stories.jsx index fca2ddfc3..4645ad9bb 100644 --- a/react/src/lib/components/DeckGLMap/storybook/DeckGLMap.stories.jsx +++ b/react/src/lib/components/DeckGLMap/storybook/DeckGLMap.stories.jsx @@ -2,6 +2,8 @@ import React from "react"; import DeckGLMap from "../DeckGLMap"; const exampleData = require("../../../../demo/example-data/deckgl-map.json"); +const colorTemplate = require("../../../../demo/example-data/welllayer_continuous_template.json"); +const colorTables = require("../../../../demo/example-data/color-tables.json"); export default { component: DeckGLMap, @@ -21,9 +23,14 @@ const Template = (args) => { setProps={(updatedProps) => { setEditedData(updatedProps.editedData); }} + colorTemplate={colorTemplate} + colorTables={colorTables} /> ); }; export const Default = Template.bind({}); Default.args = exampleData[0]; + +export const templateData = Template.bind({}); +templateData.args = { template: colorTemplate, colorTables: colorTables }; diff --git a/react/src/lib/components/DeckGLMap/utils/continuousLegend.ts b/react/src/lib/components/DeckGLMap/utils/continuousLegend.ts index 60b3d708b..29f5bff14 100644 --- a/react/src/lib/components/DeckGLMap/utils/continuousLegend.ts +++ b/react/src/lib/components/DeckGLMap/utils/continuousLegend.ts @@ -1,5 +1,88 @@ -import { scaleSequential, interpolateViridis, ScaleSequential } from "d3"; +import { color } from "d3-color"; +import { interpolateRgb } from "d3-interpolate"; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const colorTemplate = require("../../../../demo/example-data/welllayer_continuous_template.json"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const colorTables = require("../../../../demo/example-data/color-tables.json"); -export function interpolatorContinuous(): ScaleSequential { - return scaleSequential(interpolateViridis); +interface colorTables { + name: string; + description: string; + colors: [number, number, number, number][]; +} + +interface propertiesObj { + objectName: string; + colorTable: string; + context: string; + colorInterpolation: string; +} + +type propertiesArr = Array; + +interface colorTemplate { + name: string; + properties: propertiesArr; +} + +export function colorsArray( + objectName: string +): [number, number, number, number][] { + const properties = colorTemplate[0]["properties"]; + const propertiesData = properties.filter( + (value: propertiesObj) => value.objectName == objectName + ); + const colorTableData = colorTables.filter( + (value: colorTables) => + value.name.toLowerCase() == + propertiesData[0].colorTable.toLowerCase() + ); + return colorTableData[0].colors; +} + +export function rgbValues( + objectName: string, + point: number +): number[] | { r: number; g: number; b: number; opacity: number } | undefined { + const color_table = colorsArray(objectName); + const colorArrays = color_table.find( + (value: [number, number, number, number]) => { + return point == value[0]; + } + ); + + // if point and value in color table matches + if (colorArrays) { + return colorArrays.slice(1); + } + // if no match then need to do interpolation + else { + const index = color_table.findIndex((value: number[]) => { + return value[0] > point; + }); + + const firstColorArray = color_table[index - 1]; + const secondColorArray = color_table[index]; + + if ((firstColorArray || secondColorArray) != undefined) { + const interpolatedValues = interpolateRgb( + RGBToHex(firstColorArray).color, + RGBToHex(secondColorArray).color + )(point); + return color(interpolatedValues)?.rgb(); + } + return undefined; + } +} + +export function RGBToHex(rgb: number[]): { color: string; offset: number } { + let r = Math.round(rgb[1] * 255).toString(16), + g = Math.round(rgb[2] * 255).toString(16), + b = Math.round(rgb[3] * 255).toString(16); + if (r.length == 1) r = "0" + r; + if (g.length == 1) g = "0" + g; + if (b.length == 1) b = "0" + b; + const offset = rgb[0] * 100.0; + + return { color: "#" + r + g + b, offset: offset }; }