diff --git a/daiquiri/query/assets/js/components/Submit.js b/daiquiri/query/assets/js/components/Submit.js index 412bd159d..84746f7d9 100644 --- a/daiquiri/query/assets/js/components/Submit.js +++ b/daiquiri/query/assets/js/components/Submit.js @@ -33,7 +33,7 @@ const Submit = ({ formKey, jobId, query, queryLanguage, loadForm, loadJob, loadJ
{ - jobId && + jobId && } { formKey && getForm() diff --git a/daiquiri/query/assets/js/components/submit/job/Job.js b/daiquiri/query/assets/js/components/submit/job/Job.js index 7e93a5b84..b5a98ccf3 100644 --- a/daiquiri/query/assets/js/components/submit/job/Job.js +++ b/daiquiri/query/assets/js/components/submit/job/Job.js @@ -11,7 +11,7 @@ import JobResults from './JobResults' import JobPlot from './JobPlot' import JobDownload from './JobDownload' -const Job = ({ jobId, loadForm }) => { +const Job = ({ jobId, loadForm, loadJob }) => { const { data: job } = useJobQuery(jobId) const [activeTab, setActiveTab] = useLsState('daiquiri.query.job.activeTab', 'overview') @@ -71,7 +71,7 @@ const Job = ({ jobId, loadForm }) => { activeTab === 'results' && } { - activeTab === 'plot' && + activeTab === 'plot' && } { activeTab === 'download' && @@ -84,7 +84,8 @@ const Job = ({ jobId, loadForm }) => { Job.propTypes = { jobId: PropTypes.string.isRequired, - loadForm: PropTypes.func.isRequired + loadForm: PropTypes.func.isRequired, + loadJob: PropTypes.func.isRequired, } export default Job diff --git a/daiquiri/query/assets/js/components/submit/job/JobPlot.js b/daiquiri/query/assets/js/components/submit/job/JobPlot.js index 798ee230f..e363cd1ee 100644 --- a/daiquiri/query/assets/js/components/submit/job/JobPlot.js +++ b/daiquiri/query/assets/js/components/submit/job/JobPlot.js @@ -11,7 +11,7 @@ import Scatter from './plots/Scatter' import JobPlotType from './JobPlotType' -const JobPlot = ({ job }) => { +const JobPlot = ({ job, loadJob }) => { const [type, setType] = useState('scatter') const [columns, setColumns] = useState([]) @@ -22,10 +22,19 @@ const JobPlot = ({ job }) => { ))), [job]) return job.phase == 'COMPLETED' ? ( + job.nrows > 1000000 ? ( +
+
+

+ The plotting tool is available only for query results with fewer than 1 million rows. +

+
+
+ ): (
{ - (type == 'scatter') && + (type == 'scatter') && } { (type == 'colorScatter') && @@ -34,13 +43,15 @@ const JobPlot = ({ job }) => { (type == 'histogram') && }
+ ) ) : (

{jobPhaseMessage[job.phase]}

) } JobPlot.propTypes = { - job: PropTypes.object.isRequired + job: PropTypes.object.isRequired, + loadJob: PropTypes.func.isRequired, } export default JobPlot diff --git a/daiquiri/query/assets/js/components/submit/job/plots/ColorScatter.js b/daiquiri/query/assets/js/components/submit/job/plots/ColorScatter.js index 5a6781ac7..222eefceb 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/ColorScatter.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/ColorScatter.js @@ -10,7 +10,7 @@ import ColorScatterPlot from './ColorScatterPlot' const ColorScatter = ({ job, columns }) => { - const [values, setValues] = useState({ + const [plotValues, setPlotValues] = useState({ x: { column: '' }, @@ -23,27 +23,27 @@ const ColorScatter = ({ job, columns }) => { } }) - useEffect(() => setValues({ - ...values, + useEffect(() => setPlotValues({ + ...plotValues, x: { - ...values.x, column: isNil(columns[0]) ? '' : columns[0].name + ...plotValues.x, column: isNil(columns[0]) ? '' : columns[0].name }, y: { - ...values.y, column: isNil(columns[1]) ? '' : columns[1].name + ...plotValues.y, column: isNil(columns[1]) ? '' : columns[1].name }, z: { - ...values.z, column: isNil(columns[2]) ? '' : columns[2].name + ...plotValues.z, column: isNil(columns[2]) ? '' : columns[2].name } }), [columns]) - const { data: x } = useJobPlotQuery(job, values.x.column) - const { data: y } = useJobPlotQuery(job, values.y.column) - const { data: z } = useJobPlotQuery(job, values.z.column) + const { data: x } = useJobPlotQuery(job, plotValues.x.column) + const { data: y } = useJobPlotQuery(job, plotValues.y.column) + const { data: z } = useJobPlotQuery(job, plotValues.z.column) return (
- - + +
) } diff --git a/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterForm.js b/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterForm.js index ffa18d8ab..2042166c9 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterForm.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterForm.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' import { cmaps } from 'daiquiri/query/assets/js/constants/plot' -const ColorScatterForm = ({ columns, values, setValues }) => { +const ColorScatterForm = ({ columns, plotValues, setPlotValues }) => { return (
@@ -14,8 +14,8 @@ const ColorScatterForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, x: { ...plotValues.x, column: value.target.value } }) }}> { @@ -31,8 +31,8 @@ const ColorScatterForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, y: { ...plotValues.y, column: value.target.value } }) }}> { @@ -48,8 +48,8 @@ const ColorScatterForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, z: { ...plotValues.z, column: value.target.value } }) }}> { @@ -63,8 +63,8 @@ const ColorScatterForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, z: { ...plotValues.z, cmap: value.target.value } }) }}> { cmaps.map((cmap, cmapIndex) => ) @@ -79,8 +79,8 @@ const ColorScatterForm = ({ columns, values, setValues }) => { ColorScatterForm.propTypes = { columns: PropTypes.array.isRequired, - values: PropTypes.object.isRequired, - setValues: PropTypes.func.isRequired + plotValues: PropTypes.object.isRequired, + setPlotValues: PropTypes.func.isRequired } export default ColorScatterForm diff --git a/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterPlot.js b/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterPlot.js index 7cae74332..85f5be0fa 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterPlot.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/ColorScatterPlot.js @@ -6,7 +6,7 @@ import { isNil } from 'lodash' import { config, layout } from 'daiquiri/query/assets/js/constants/plot' import { getColumnLabel } from 'daiquiri/query/assets/js/utils/plot' -const ColorScatterPlot = ({ columns, values, x, y, z }) => { +const ColorScatterPlot = ({ columns, plotValues, x, y, z }) => { if (isNil(x) || isNil(y) || isNil(z)) { return null } else { @@ -24,30 +24,31 @@ const ColorScatterPlot = ({ columns, values, x, y, z }) => { marker: { showscale: true, color: z, - colorscale: values.z.cmap, + colorscale: plotValues.z.cmap, colorbar: { title: { - text: getColumnLabel(columns, values.z.column), + text: getColumnLabel(columns, plotValues.z.column), side: 'right' } } }, + hoverinfo: 'skip' } ]} layout={{ ...layout, xaxis: { title: { - text: getColumnLabel(columns, values.x.column), + text: getColumnLabel(columns, plotValues.x.column), }, }, yaxis: { title: { - text: getColumnLabel(columns, values.y.column), + text: getColumnLabel(columns, plotValues.y.column), } } }} - style={{width: '100%'}} + style={{ width: '100%' }} useResizeHandler={true} config={config} /> @@ -60,7 +61,7 @@ const ColorScatterPlot = ({ columns, values, x, y, z }) => { ColorScatterPlot.propTypes = { columns: PropTypes.array.isRequired, - values: PropTypes.object.isRequired, + plotValues: PropTypes.object.isRequired, x: PropTypes.array, y: PropTypes.array, z: PropTypes.array diff --git a/daiquiri/query/assets/js/components/submit/job/plots/Histogram.js b/daiquiri/query/assets/js/components/submit/job/plots/Histogram.js index 61b87d701..c4b8e7a24 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/Histogram.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/Histogram.js @@ -10,7 +10,7 @@ import HistogramPlot from './HistogramPlot' const Histogram = ({ job, columns }) => { - const [values, setValues] = useState({ + const [plotValues, setPlotValues] = useState({ x: { column: '' }, @@ -22,20 +22,20 @@ const Histogram = ({ job, columns }) => { bins: 20, }) - useEffect(() => setValues({ - ...values, + useEffect(() => setPlotValues({ + ...plotValues, x: { - ...values.x, column: isNil(columns[0]) ? '' : columns[0].name + ...plotValues.x, column: isNil(columns[0]) ? '' : columns[0].name } }), [columns]) - const { data: x } = useJobPlotQuery(job, values.x.column) - const { data: s } = useJobPlotQuery(job, values.s.column) + const { data: x } = useJobPlotQuery(job, plotValues.x.column) + const { data: s } = useJobPlotQuery(job, plotValues.s.column) return (
- - + +
) } diff --git a/daiquiri/query/assets/js/components/submit/job/plots/HistogramForm.js b/daiquiri/query/assets/js/components/submit/job/plots/HistogramForm.js index 582bcc353..468f98b48 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/HistogramForm.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/HistogramForm.js @@ -4,12 +4,12 @@ import { useDebouncedCallback } from 'use-debounce' import { operations } from 'daiquiri/query/assets/js/constants/plot' -const HistogramForm = ({ columns, values, setValues }) => { - const setBins = useDebouncedCallback((event) => setValues({ - ...values, bins: event.target.value +const HistogramForm = ({ columns, plotValues, setPlotValues }) => { + const setBins = useDebouncedCallback((event) => setPlotValues({ + ...plotValues, bins: event.target.value }), 500) - const setSelectValue = useDebouncedCallback((event) => setValues({ - ...values, s: {...values.s, value: event.target.value} + const setSelectValue = useDebouncedCallback((event) => setPlotValues({ + ...plotValues, s: { ...plotValues.s, value: event.target.value } }), 500) return ( @@ -22,8 +22,8 @@ const HistogramForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, x: { ...plotValues.x, column: event.target.value } }) }}> { @@ -39,7 +39,7 @@ const HistogramForm = ({ columns, values, setValues }) => {
- +
@@ -49,8 +49,8 @@ const HistogramForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, s: { ...plotValues.s, column: event.target.value } }) }}> { @@ -59,8 +59,8 @@ const HistogramForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, s: { ...plotValues.s, operation: event.target.value } }) }}> { operations.map((operation, operationIndex) => ) @@ -68,7 +68,7 @@ const HistogramForm = ({ columns, values, setValues }) => {
- +
@@ -78,8 +78,8 @@ const HistogramForm = ({ columns, values, setValues }) => { HistogramForm.propTypes = { columns: PropTypes.array.isRequired, - values: PropTypes.object.isRequired, - setValues: PropTypes.func.isRequired + plotValues: PropTypes.object.isRequired, + setPlotValues: PropTypes.func.isRequired } export default HistogramForm diff --git a/daiquiri/query/assets/js/components/submit/job/plots/HistogramPlot.js b/daiquiri/query/assets/js/components/submit/job/plots/HistogramPlot.js index 891b7ea92..ae04040a4 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/HistogramPlot.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/HistogramPlot.js @@ -6,7 +6,7 @@ import { isNil } from 'lodash' import { config, layout, operations } from 'daiquiri/query/assets/js/constants/plot' import { getColumnLabel } from 'daiquiri/query/assets/js/utils/plot' -const HistogramPlot = ({ columns, values, x, s }) => { +const HistogramPlot = ({ columns, plotValues, x, s }) => { if (isNil(x)) { return null } else { @@ -14,17 +14,17 @@ const HistogramPlot = ({ columns, values, x, s }) => { { x: x, type: 'histogram', - nbinsx: values.bins + nbinsx: plotValues.bins } ] if (!isNil(s)) { - const operation = operations.find(operation => operation.name == values.s.operation) || operations[0] + const operation = operations.find(operation => operation.name == plotValues.s.operation) || operations[0] data.push({ - x: x.filter((value, index) => operation.operation(s[index], values.s.value)), + x: x.filter((value, index) => operation.operation(s[index], plotValues.s.value)), type: 'histogram', - nbinsx: values.bins + nbinsx: plotValues.bins }) } @@ -39,7 +39,7 @@ const HistogramPlot = ({ columns, values, x, s }) => { bargap: 0.1, xaxis: { title: { - text: getColumnLabel(columns, values.x.column), + text: getColumnLabel(columns, plotValues.x.column), }, }, yaxis: { @@ -48,7 +48,7 @@ const HistogramPlot = ({ columns, values, x, s }) => { } } }} - style={{width: '100%'}} + style={{ width: '100%' }} useResizeHandler={true} config={config} /> @@ -61,7 +61,7 @@ const HistogramPlot = ({ columns, values, x, s }) => { HistogramPlot.propTypes = { columns: PropTypes.array.isRequired, - values: PropTypes.object.isRequired, + plotValues: PropTypes.object.isRequired, x: PropTypes.array, s: PropTypes.array, } diff --git a/daiquiri/query/assets/js/components/submit/job/plots/Scatter.js b/daiquiri/query/assets/js/components/submit/job/plots/Scatter.js index 88f73e34d..84e93828c 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/Scatter.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/Scatter.js @@ -8,55 +8,44 @@ import { useJobPlotQuery } from 'daiquiri/query/assets/js/hooks/queries' import ScatterForm from './ScatterForm' import ScatterPlot from './ScatterPlot' -const Scatter = ({ job, columns }) => { +const Scatter = ({ job, columns, loadJob }) => { - const [values, setValues] = useState({ + const [plotValues, setPlotValues] = useState({ x: { column: '', }, - y1: { + y: { column: '', color: colors[0].hex, symbol: symbols[0].symbol - }, - y2: { - column: '', - color: colors[1].hex, - symbol: symbols[1].symbol - }, - y3: { - column: '', - color: colors[2].hex, - symbol: symbols[2].symbol } }) - useEffect(() => setValues({ - ...values, + useEffect(() => setPlotValues({ + ...plotValues, x: { - ...values.x, column: isNil(columns[0]) ? '' : columns[0].name + ...plotValues.x, column: isNil(columns[0]) ? '' : columns[0].name }, - y1: { - ...values.y1, column: isNil(columns[1]) ? '' : columns[1].name + y: { + ...plotValues.y, column: isNil(columns[1]) ? '' : columns[1].name } }), [columns]) - const { data: x } = useJobPlotQuery(job, values.x.column) - const { data: y1 } = useJobPlotQuery(job, values.y1.column) - const { data: y2 } = useJobPlotQuery(job, values.y2.column) - const { data: y3 } = useJobPlotQuery(job, values.y3.column) + const { data: x } = useJobPlotQuery(job, plotValues.x.column) + const { data: y } = useJobPlotQuery(job, plotValues.y.column) return (
- - + + {job && }
) } Scatter.propTypes = { job: PropTypes.object.isRequired, - columns: PropTypes.array.isRequired + columns: PropTypes.array.isRequired, + loadJob: PropTypes.func.isRequired, } export default Scatter diff --git a/daiquiri/query/assets/js/components/submit/job/plots/ScatterForm.js b/daiquiri/query/assets/js/components/submit/job/plots/ScatterForm.js index f77e53490..6b0c0c12d 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/ScatterForm.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/ScatterForm.js @@ -3,13 +3,7 @@ import PropTypes from 'prop-types' import { colors, symbols } from 'daiquiri/query/assets/js/constants/plot' -const ScatterForm = ({ columns, values, setValues }) => { - - const labels = { - y1: Y1, - y2: Y2, - y3: Y3 - } +const ScatterForm = ({ columns, plotValues, setPlotValues }) => { const getSymbolHtml = (symbol) => { const s = symbols.find(s => s.symbol == symbol) @@ -28,8 +22,8 @@ const ScatterForm = ({ columns, values, setValues }) => {
- { + setPlotValues({ ...plotValues, x: { ...plotValues.x, column: value.target.value } }) }}> { @@ -39,46 +33,44 @@ const ScatterForm = ({ columns, values, setValues }) => {
{ - ['y1', 'y2', 'y3'].map(y => ( -
-
- -
-
- -
-
-
{getSymbolHtml(values[y].symbol)}
-
-
- -
-
- -
+
+
+ +
+
+ +
+
+
{getSymbolHtml(plotValues.y.symbol)}
- )) +
+ +
+
+ +
+
}
@@ -87,8 +79,8 @@ const ScatterForm = ({ columns, values, setValues }) => { ScatterForm.propTypes = { columns: PropTypes.array.isRequired, - values: PropTypes.object.isRequired, - setValues: PropTypes.func.isRequired + plotValues: PropTypes.object.isRequired, + setPlotValues: PropTypes.func.isRequired } export default ScatterForm diff --git a/daiquiri/query/assets/js/components/submit/job/plots/ScatterPlot.js b/daiquiri/query/assets/js/components/submit/job/plots/ScatterPlot.js index cd42cb03f..51078f389 100644 --- a/daiquiri/query/assets/js/components/submit/job/plots/ScatterPlot.js +++ b/daiquiri/query/assets/js/components/submit/job/plots/ScatterPlot.js @@ -1,78 +1,200 @@ -import React from 'react' +import React, { useState, useRef, useCallback, memo } from 'react' import PropTypes from 'prop-types' import Plot from 'react-plotly.js' -import { isNil } from 'lodash' +import { get, isNil, toString } from 'lodash' +import classNames from 'classnames' import { config, layout } from 'daiquiri/query/assets/js/constants/plot' import { getColumnLabel } from 'daiquiri/query/assets/js/utils/plot' +import { + useQueryLanguagesQuery, + useQueuesQuery, +} from 'daiquiri/query/assets/js/hooks/queries' +import { useSubmitJobMutation } from 'daiquiri/query/assets/js/hooks/mutations' -const ScatterPlot = ({ columns, values, x, y1, y2, y3 }) => { - if (isNil(x)) { - return null - } else { - const data = [[values.y1, y1], [values.y2, y2], [values.y3, y3]].reduce((data, [yValues, y]) => { - if (isNil(y)) { - return data - } else { - return [...data, { - x: x, - y: y, - type: 'scattergl', - mode: 'markers', - marker: { - color: yValues.color, - symbol: yValues.symbol +import Errors from 'daiquiri/core/assets/js/components/form/Errors' +import Input from 'daiquiri/core/assets/js/components/form/Input' +import Select from 'daiquiri/core/assets/js/components/form/Select' + +const MemoizedPlot = memo(({ columns, plotValues, x, y, onSelected }) => { + + const opacity = x.length > 100000 ? 0.25 : 0.6 + + const data = [{ + x: x, + y: y, + type: 'scattergl', + mode: 'markers', + opacity: opacity, + marker: { + size: 5, + color: plotValues.y.color, + symbol: plotValues.y.symbol + }, + hoverinfo: 'skip' + }] + + const yLabel = getColumnLabel(columns, plotValues.y.column) + + return ( + + ); +}); + + +const ScatterPlot = ({ columns, plotValues, x, y, loadJob, job }) => { + const selectedPointsRef = useRef({ x: [], y: [], n: 0 }) + const [errors, setErrors] = useState({}) + const [isFormDisabled, setIsFormDisabled] = useState(true) + + const [values, setValues] = useState({ + table_name: '', + run_id: '', + queue: '', + query: '', + query_language: '', + }) + + const { data: queues } = useQueuesQuery() + const { data: queryLanguages } = useQueryLanguagesQuery() + + const mutation = useSubmitJobMutation() + + const getDefaultQueue = () => (isNil(queues) ? '' : queues[0].id) + + const handleSubmit = () => { + const polyPoints = selectedPointsRef.current.x + .map((x, index) => `(${x}, ${selectedPointsRef.current.y[index]})`) + .concat(`(${selectedPointsRef.current.x[0]}, ${selectedPointsRef.current.y[0]})`) + .join(', '); + const query = `SELECT * +FROM "${job.schema_name}"."${job.table_name}" as t +WHERE POLYGON '(${polyPoints})' @> POINT(t.${plotValues.x.column}, t.${plotValues.y.column});` - const yLabel = [[values.y1, y1], [values.y2, y2], [values.y3, y3]].reduce((label, [yValues, y]) => { - if (isNil(y)) { - return label - } else { - return [...label, getColumnLabel(columns, yValues.column)] + const postgres = queryLanguages.find(language => toString(language.id).includes('postgres')) + values.query_language = postgres ? postgres.id : job.query_language + values.query = query + values.queue = values.queue == '' ? getDefaultQueue() : values.queue + + mutation.mutate({ values: values, setErrors, loadJob }) + } + + const handleSelection = useCallback((event) => { + if (event && event.lassoPoints && event.lassoPoints.x.length > 0) { + selectedPointsRef.current = { x: event.lassoPoints.x, y: event.lassoPoints.y, n: event.points.length } + setIsFormDisabled(false) + } else if (event && event.range && event.range.x.length > 0) { + selectedPointsRef.current = { + x: [event.range.x[0], event.range.x[0], event.range.x[1], event.range.x[1]], + y: [event.range.y[0], event.range.y[1], event.range.y[1], event.range.y[0]], + n: event.points.length } - }, []).join(', ') - - return ( -
-
-
- -
+ setIsFormDisabled(false) + } else { + selectedPointsRef.current = { x: [], y: [], n: 0 } + setIsFormDisabled(true) + } + }, []); + + if (isNil(x)) { + return null + } + + return ( +
+
+
+
- ) - } + { + isFormDisabled ? ( +
+ Select points with the 'Lasso' or 'Box' tool to run a new query on the selected subset. +
+ ) : ( +
+
+
You have selected {selectedPointsRef.current.n} points. Please click 'Submit Query' to submit a new query for the selected region.
+
+
+
+ setValues({ ...values, table_name })} + /> +
+
+ setValues({ ...values, run_id })} + /> +
+
+