Finish chart editing support
This commit is contained in:
parent
619d768cc3
commit
2818c7b09f
110
src/components/RangeChart.vue
Normal file
110
src/components/RangeChart.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import * as math from 'mathjs'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import {LineChart} from 'vue-chart-3'
|
||||
import {computed, ref} from 'vue'
|
||||
import {Chart, ChartData, registerables} from 'chart.js'
|
||||
import {MathStatement} from '../support/parse'
|
||||
import {ChartBox} from '../support/types'
|
||||
import {stepX, stepY} from '../support/const'
|
||||
|
||||
Chart.register(...registerables)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(eventName: 'move', x: number, y:number): void,
|
||||
(eventName: 'edit'): void,
|
||||
(eventName: 'remove'): void,
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
fn: MathStatement,
|
||||
value: ChartBox,
|
||||
}>()
|
||||
|
||||
const getChartData = (): ChartData<'line'> => {
|
||||
const range = []
|
||||
const min = Math.min(props.value.minX, props.value.maxX)
|
||||
const max = Math.max(props.value.minX, props.value.maxX)
|
||||
for ( let i = min; i <= max; i += parseFloat(String(props.value.stepX)) || 1 ) {
|
||||
range.push(i)
|
||||
}
|
||||
|
||||
if ( !props.fn.isFunctionDeclaration() ) {
|
||||
throw new TypeError('Cannot chart node that is not a function.')
|
||||
}
|
||||
|
||||
const node = props.fn.parse() as math.FunctionAssignmentNode
|
||||
const fn = node.compile().evaluate() // FIXME need dependencies in scope
|
||||
|
||||
console.log('getChartData', {
|
||||
labels: range.map(x => `${x}`),
|
||||
datasets: [
|
||||
{
|
||||
label: node.name,
|
||||
data: range.map(n => fn(n)),
|
||||
},
|
||||
],
|
||||
})
|
||||
return {
|
||||
labels: range.map(x => `${x}`),
|
||||
datasets: [{
|
||||
label: node.name,
|
||||
backgroundColor: '#553564',
|
||||
data: range.map(x => fn(x)),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = ref(getChartData())
|
||||
const chartKey = ref(uuidv4())
|
||||
computed(() => {
|
||||
chartData.value = getChartData()
|
||||
chartKey.value = uuidv4()
|
||||
})
|
||||
|
||||
function onControlledDrag(e: {event: MouseEvent, data: {x: number, y: number}}) {
|
||||
|
||||
// const x = e.x;
|
||||
// const y = e.y;
|
||||
const { x, y } = e.data;
|
||||
props.value.x = x;
|
||||
props.value.y = y;
|
||||
console.log(e)
|
||||
}
|
||||
function onControlledDragStop(e: {event: MouseEvent, data: {x: number, y: number}}) {
|
||||
// console.log(typeof(e))
|
||||
const { x, y } = e.data;
|
||||
// const x = e.x;
|
||||
// const y = e.y;
|
||||
console.log(self)
|
||||
emit('move', x, y);
|
||||
onControlledDrag(e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Draggable
|
||||
:grid="[stepX, stepY]"
|
||||
:default-position="{ x: props.value.x, y: props.value.y }"
|
||||
:position="{ x: props.value.x, y: props.value.y }"
|
||||
@stop="onControlledDragStop"
|
||||
>
|
||||
<div style="background: white; display: flex; flex-direction: row; border: 1px solid #ccc; border-radius: 3px;">
|
||||
<LineChart :chartData="chartData" :key="chartKey" class="inner-chart"/>
|
||||
<div class="sidebar">
|
||||
<q-btn color="grey-7" round flat icon="more_vert">
|
||||
<q-menu cover auto-close>
|
||||
<q-list>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => $emit('edit')">Edit</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => $emit('remove')">Remove</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</template>
|
@ -3,7 +3,7 @@ import {onMounted, ref} from 'vue'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import {MathPage} from '../support/page'
|
||||
import {MathStatement} from '../support/parse'
|
||||
import {EvaluationResult, hasOwnProperty} from '../support/types'
|
||||
import {ChartBox, EvaluationResult, hasOwnProperty} from '../support/types'
|
||||
import Statement from '../components/Statement.vue'
|
||||
import VarDeclEditor from './VarDeclEditor.vue'
|
||||
import ExpressionEditor from './ExpressionEditor.vue'
|
||||
@ -11,6 +11,8 @@ import TextBox from '../components/TextBox.vue'
|
||||
import {RichTextBox} from '../support/types'
|
||||
import { stepX, stepY } from '../support/const'
|
||||
import FunctionEditor from '../components/FunctionEditor.vue'
|
||||
import RangeChart from '../components/RangeChart.vue'
|
||||
import RangeChartEditor from './RangeChartEditor.vue'
|
||||
|
||||
const math = new MathPage(uuidv4());
|
||||
const statements = ref<MathStatement[]>([]);
|
||||
@ -185,6 +187,39 @@ const makeNewRichTextBox = () => {
|
||||
richEditModal.value = true;
|
||||
};
|
||||
|
||||
const chartBoxKey = ref(uuidv4())
|
||||
const chartBoxes = ref<ChartBox[]>([])
|
||||
|
||||
const newChartModalOpen = ref(false)
|
||||
const openNewChartModal = () => {
|
||||
newChartModalOpen.value = true
|
||||
}
|
||||
|
||||
const saveNewChartBox = (chartBox: ChartBox) => {
|
||||
chartBoxes.value.push(chartBox)
|
||||
newChartModalOpen.value = false
|
||||
}
|
||||
|
||||
const editingChartBox = ref<ChartBox|undefined>()
|
||||
const chartEditModalOpen = ref(false)
|
||||
const openChartEditModal = (box: ChartBox) => {
|
||||
editingChartBox.value = box
|
||||
chartEditModalOpen.value = true
|
||||
}
|
||||
|
||||
const saveEditingChartBox = () => {
|
||||
chartEditModalOpen.value = false
|
||||
chartBoxKey.value = uuidv4()
|
||||
}
|
||||
|
||||
const moveChartBox = (id: number, x: number, y: number) => {
|
||||
chartBoxes.value[id].x = x
|
||||
chartBoxes.value[id].y = y
|
||||
}
|
||||
const removeChartBox = (id: number) => {
|
||||
chartBoxes.value.splice(id, 1);
|
||||
};
|
||||
|
||||
const richTextStatements = ref<RichTextBox[]>([]);
|
||||
|
||||
const richEditModal = ref(false);
|
||||
@ -290,6 +325,17 @@ const removeRichTextBox = (id: number) => {
|
||||
<q-page-container id="editor">
|
||||
<!-- <WrapperBox />-->
|
||||
|
||||
<span v-for="(chartBox, index) in chartBoxes" style="display: flex">
|
||||
<RangeChart
|
||||
:fn="math.getFunctionByName(chartBox.fnName)"
|
||||
:key="chartBoxKey"
|
||||
:value="chartBox"
|
||||
v-on:move="(x, y) => moveChartBox(index, x, y)"
|
||||
v-on:remove="() => removeChartBox(index)"
|
||||
v-on:edit="() => openChartEditModal(chartBox)"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span v-for="statement in statements" style="display: flex">
|
||||
<Draggable
|
||||
:grid="[stepX, stepY]"
|
||||
@ -344,6 +390,21 @@ const removeRichTextBox = (id: number) => {
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="newChartModalOpen">
|
||||
<RangeChartEditor
|
||||
:page="math"
|
||||
v-on:save="c => saveNewChartBox(c)"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="chartEditModalOpen">
|
||||
<RangeChartEditor
|
||||
:page="math"
|
||||
:chartBox="editingChartBox"
|
||||
v-on:save="() => saveEditingChartBox()"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
<q-page-sticky position="bottom-right" :offset="[32, 32]">
|
||||
<q-fab color="primary" icon="add" direction="left">
|
||||
<q-fab-action
|
||||
@ -367,10 +428,16 @@ const removeRichTextBox = (id: number) => {
|
||||
/>
|
||||
<q-fab-action
|
||||
color="secondary"
|
||||
icon="text"
|
||||
icon="format_quote"
|
||||
title="Add a text box"
|
||||
@click="() => makeNewRichTextBox()"
|
||||
/>
|
||||
<q-fab-action
|
||||
color="secondary"
|
||||
icon="show_chart"
|
||||
title="Add a new chart"
|
||||
@click="() => openNewChartModal()"
|
||||
/>
|
||||
</q-fab>
|
||||
</q-page-sticky>
|
||||
|
||||
|
118
src/pages/RangeChartEditor.vue
Normal file
118
src/pages/RangeChartEditor.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import * as math from 'mathjs'
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {MathStatement} from '../support/parse'
|
||||
import {v4 as uuidv4} from 'uuid'
|
||||
import Katex from '../components/Katex.vue'
|
||||
import {ChartBox, StatementID} from '../support/types'
|
||||
import {MathPage} from '../support/page'
|
||||
|
||||
const props = defineProps<{
|
||||
page: MathPage,
|
||||
chartBox?: ChartBox,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(eventName: 'save', statement: ChartBox): void,
|
||||
}>()
|
||||
|
||||
const chartBoxModalError = ref<string|undefined>(undefined)
|
||||
const chartBoxFunctionName = ref('')
|
||||
const chartBoxMinXValue = ref(0)
|
||||
const chartBoxMaxXValue = ref(30)
|
||||
const chartBoxStepXValue = ref(1)
|
||||
|
||||
const validateChartBox = () => {
|
||||
if ( !chartBoxFunctionName.value ) {
|
||||
return 'Missing function name'
|
||||
}
|
||||
|
||||
if ( isNaN(parseFloat(String(chartBoxMinXValue.value))) ) {
|
||||
return 'Invalid minimum X-value'
|
||||
}
|
||||
|
||||
if ( isNaN(parseFloat(String(chartBoxMaxXValue.value))) ) {
|
||||
return 'Invalid maximum X-value'
|
||||
}
|
||||
|
||||
if ( isNaN(parseFloat(String(chartBoxStepXValue.value))) ) {
|
||||
return 'Invalid X-value step size'
|
||||
}
|
||||
|
||||
if ( !props.page.getFunctionByName(chartBoxFunctionName.value) ) {
|
||||
return 'Invalid function name'
|
||||
}
|
||||
}
|
||||
|
||||
const saveChartBox = () => {
|
||||
chartBoxModalError.value = validateChartBox()
|
||||
if ( chartBoxModalError.value ) {
|
||||
return
|
||||
}
|
||||
|
||||
if ( props.chartBox ) {
|
||||
props.chartBox.fnName = chartBoxFunctionName.value
|
||||
props.chartBox.minX = chartBoxMinXValue.value
|
||||
props.chartBox.maxX = chartBoxMaxXValue.value
|
||||
props.chartBox.stepX = chartBoxStepXValue.value
|
||||
emit('save', props.chartBox)
|
||||
} else {
|
||||
emit('save', new ChartBox(
|
||||
chartBoxFunctionName.value,
|
||||
chartBoxMinXValue.value,
|
||||
chartBoxMaxXValue.value,
|
||||
chartBoxStepXValue.value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if ( props.chartBox ) {
|
||||
chartBoxFunctionName.value = props.chartBox.fnName
|
||||
chartBoxMinXValue.value = props.chartBox.minX
|
||||
chartBoxMaxXValue.value = props.chartBox.maxX
|
||||
chartBoxStepXValue.value = props.chartBox.stepX
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card>
|
||||
<q-card-section v-if="chartBoxModalError">
|
||||
<div style="color: darkred; font-weight: bold">{{ chartBoxModalError }}</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-input
|
||||
v-model="chartBoxFunctionName"
|
||||
outlined
|
||||
autofocus
|
||||
label="Function name"
|
||||
/>
|
||||
<br>
|
||||
<q-input
|
||||
v-model="chartBoxMinXValue"
|
||||
outlined
|
||||
autofocus
|
||||
label="Minimum X-value"
|
||||
/>
|
||||
<br>
|
||||
<q-input
|
||||
v-model="chartBoxMaxXValue"
|
||||
outlined
|
||||
autofocus
|
||||
label="Maximum X-value"
|
||||
/>
|
||||
<br>
|
||||
<q-input
|
||||
v-model="chartBoxStepXValue"
|
||||
outlined
|
||||
autofocus
|
||||
label="X-value step size"
|
||||
/>
|
||||
</q-card-section>
|
||||
<q-card-actions align="right" class="text-primary">
|
||||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||
<q-btn flat label="Save" @click="() => saveChartBox()"></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
@ -128,6 +128,16 @@ export class MathPage {
|
||||
.filter(x => x.isFunctionDeclaration())
|
||||
}
|
||||
|
||||
/** Look up a function statement by name, if it exists. */
|
||||
getFunctionByName(name: string): MathStatement|undefined {
|
||||
for ( const fn of this.functions() ) {
|
||||
const node = fn.parse() as math.FunctionAssignmentNode
|
||||
if ( node.name === name ) {
|
||||
return fn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Evaluate the current state of the page and get the result. */
|
||||
evaluate(): EvaluationResult {
|
||||
const evaluations: Record<StatementID, any> = {}
|
||||
|
@ -111,3 +111,16 @@ export class ImageBox {
|
||||
}
|
||||
|
||||
|
||||
export class ChartBox {
|
||||
// eslint-disable-next-line max-params
|
||||
constructor(
|
||||
public fnName: string,
|
||||
public minX: number,
|
||||
public maxX: number,
|
||||
public stepX: number = 1,
|
||||
public x: number = 0,
|
||||
public y: number = 0,
|
||||
) {}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user