YAxisStep as expression, selection box for X & Y axis labels, simplifications for expressions.

This commit is contained in:
Ad5001 2020-12-24 00:06:52 +01:00
parent 487daa426a
commit 47a4ac67a9
11 changed files with 232 additions and 111 deletions

2
.gitignore vendored
View file

@ -14,7 +14,7 @@ dist/
build
docs/html
.directory
loggraph.kdev4
*.kdev4
*.json
.kdev4
AccountFree.pro

View file

@ -27,20 +27,20 @@ MenuBar {
text: qsTr("&Load...")
shortcut: StandardKey.Open
onTriggered: settings.load()
icon.name: 'fileopen'
icon.name: 'document-open'
}
Action {
text: qsTr("&Save")
shortcut: StandardKey.Save
onTriggered: settings.save()
icon.name: 'filesave'
icon.name: 'document-save'
}
Action {
text: qsTr("Save &As...")
shortcut: StandardKey.SaveAs
onTriggered: settings.saveAs()
icon.name: 'filesaveas'
icon.name: 'document-save-as'
}
MenuSeparator { }
@ -57,7 +57,7 @@ MenuBar {
text: qsTr("&Copy diagram")
shortcut: StandardKey.Copy
onTriggered: root.copyDiagramToClipboard()
icon.name: 'editcopy'
icon.name: 'edit-copy'
}
}
Menu {

View file

@ -24,10 +24,16 @@ Item {
height: 30
signal activated(int newIndex)
signal accepted()
property var model: []
property string label: ''
property alias model: combox.model
property alias editable: combox.editable
property alias editText: combox.editText
property alias currentIndex: combox.currentIndex
function find(elementName) {
return combox.find(elementName)
}
Text {
id: labelItem
@ -43,11 +49,10 @@ Item {
height: 30
anchors.left: labelItem.right
anchors.leftMargin: 5
width: control.width - labelItem.width
model: control.model
currentIndex: model.indexOf(defValue)
width: control.width - labelItem.width - 5
onActivated: function(newIndex) {
control.activated(newIndex)
}
onAccepted: control.accepted()
}
}

View file

@ -19,6 +19,7 @@
import QtQuick 2.12
import "js/objects.js" as Objects
import "js/utils.js" as Utils
import "js/mathlib.js" as MathLib
Canvas {
@ -32,12 +33,18 @@ Canvas {
property double ymax: 0
property int xzoom: 10
property int yzoom: 10
property double yaxisstep: 3
property string yaxisstep: "3"
property string xlabel: ""
property string ylabel: ""
property int maxgradx: 8
property int maxgrady: 1000
property var yaxisstepExpr: (new MathLib.Expression(`x*(${yaxisstep})`))
property double yaxisstep1: yaxisstepExpr.execute(1)
property int drawMaxY: Math.ceil(Math.max(Math.abs(ymax), Math.abs(px2y(canvasSize.height)))/yaxisstep1)
Component.onCompleted: console.log(yaxisstepExpr.toEditableString())
onPaint: {
//console.log('Redrawing')
var ctx = getContext("2d");
@ -71,8 +78,9 @@ Canvas {
drawXLine(ctx, Math.pow(10, xpow)*xmulti)
}
}
for(var y = -Math.round(100/yaxisstep)*yaxisstep; y < canvas.ymax; y+=yaxisstep) {
drawYLine(ctx, y)
for(var y = 0; y < drawMaxY; y+=1) {
drawYLine(ctx, y*yaxisstep1)
drawYLine(ctx, -y*yaxisstep1)
}
}
@ -99,16 +107,21 @@ Canvas {
var textSize = ctx.measureText(canvas.xlabel).width
ctx.fillText(canvas.xlabel, canvas.canvasSize.width-14-textSize, axisxpx-5)
// Axis graduation labels
ctx.font = "14px sans-serif"
ctx.font = "12px sans-serif"
for(var xpow = -maxgradx; xpow <= maxgradx; xpow+=1) {
var textSize = ctx.measureText("10"+Utils.textsup(xpow)).width
if(xpow != 0)
drawVisibleText(ctx, "10"+Utils.textsup(xpow), x2px(Math.pow(10,xpow))-textSize/2, axisxpx+12+(6*(y==0)))
drawVisibleText(ctx, "10"+Utils.textsup(xpow), x2px(Math.pow(10,xpow))-textSize/2, axisxpx+16+(6*(y==0)))
}
for(var y = -Math.round(maxgrady/yaxisstep)*yaxisstep; y < canvas.ymax; y+=yaxisstep) {
var textSize = ctx.measureText(y).width
drawVisibleText(ctx, y, axisypx-3-textSize, y2px(y)+6+(6*(y==0)))
var txtMinus = ctx.measureText('-').width
for(var y = 0; y < drawMaxY; y += 1) {
var drawY = y*yaxisstep1
var txtY = yaxisstepExpr.simplify(y)
var textSize = ctx.measureText(txtY).width
drawVisibleText(ctx, txtY, axisypx-6-textSize, y2px(drawY)+4+(10*(y==0)))
if(y != 0)
drawVisibleText(ctx, '-'+txtY, axisypx-6-textSize-txtMinus, y2px(-drawY)+4)
}
ctx.fillStyle = "#FFFFFF"
}

View file

@ -201,7 +201,7 @@ ListView {
isDouble: modelData[1] == 'number'
visible: ['Expression', 'Domain', 'string', 'number'].indexOf(modelData[1]) >= 0
defValue: visible ? {
'Expression': function(){return objEditor.obj[modelData[0]].toEditableString()},
'Expression': function(){return Utils.simplifyExpression(objEditor.obj[modelData[0]].toEditableString())},
'Domain': function(){return objEditor.obj[modelData[0]].toString()},
'string': function(){return objEditor.obj[modelData[0]]},
'number': function(){return objEditor.obj[modelData[0]]}

View file

@ -20,23 +20,23 @@ import QtQuick.Controls 2.12
import QtQuick 2.12
import "js/utils.js" as Utils
Grid {
Column {
id: settings
height: 30*Math.max(1, Math.ceil(7 / columns))
columns: Math.floor(width / settingWidth)
height: 30*9 //30*Math.max(1, Math.ceil(7 / columns))
//columns: Math.floor(width / settingWidth)
spacing: 10
signal changed()
property int settingWidth: 135
property int settingWidth: settings.width
property int xzoom: 100
property int yzoom: 10
property double xmin: 5/10
property double ymax: 25
property int yaxisstep: 4
property string xaxislabel: "ω (rad/s)"
property string yaxislabel: "G (dB)"
property string yaxisstep: "4"
property string xaxislabel: ""
property string yaxislabel: ""
property string saveFilename: ""
FileDialog {
@ -48,6 +48,10 @@ Grid {
root.saveDiagram(filePath)
} else {
root.loadDiagram(filePath)
if(xAxisLabel.find(settings.xaxislabel) == -1) xAxisLabel.model.append({text: settings.xaxislabel})
xAxisLabel.editText = settings.xaxislabel
if(yAxisLabel.find(settings.yaxislabel) == -1) yAxisLabel.model.append({text: settings.yaxislabel})
yAxisLabel.editText = settings.yaxislabel
}
}
}
@ -108,7 +112,7 @@ Grid {
TextSetting {
id: yAxisStep
height: 30
isInt: true
//isInt: true
label: "Y Axis Step"
width: settings.settingWidth
defValue: settings.yaxisstep
@ -118,6 +122,58 @@ Grid {
}
}
ComboBoxSetting {
id: xAxisLabel
height: 30
width: settings.settingWidth
label: 'X Label'
model: ListModel {
ListElement { text: "" }
ListElement { text: "x" }
ListElement { text: "ω (rad/s)" }
}
currentIndex: find(settings.xaxislabel)
editable: true
onAccepted: function(){
editText = Utils.parseName(editText, false)
if (find(editText) === -1) model.append({text: editText})
settings.xaxislabel = editText
settings.changed()
}
onActivated: function(selectedId) {
settings.xaxislabel = model.get(selectedId).text
settings.changed()
}
Component.onCompleted: editText = settings.xaxislabel
}
ComboBoxSetting {
id: yAxisLabel
height: 30
width: settings.settingWidth
label: 'Y Label'
model: ListModel {
ListElement { text: "" }
ListElement { text: "y" }
ListElement { text: "G (dB)" }
ListElement { text: "φ (deg)" }
ListElement { text: "φ (rad)" }
}
currentIndex: find(settings.yaxislabel)
editable: true
onAccepted: function(){
editText = Utils.parseName(editText, false)
if (find(editText) === -1) model.append({text: editText, yaxisstep: root.yaxisstep})
settings.yaxislabel = editText
settings.changed()
}
onActivated: function(selectedId) {
settings.yaxislabel = model.get(selectedId).text
settings.changed()
}
Component.onCompleted: editText = settings.yaxislabel
}
Button {
id: copyToClipboard
height: 30
@ -127,30 +183,6 @@ Grid {
onClicked: root.copyDiagramToClipboard()
}
TextSetting {
id: xAxisLabel
height: 30
label: "X Label"
width: settings.settingWidth
defValue: settings.xaxislabel
onChanged: function(newValue) {
settings.xaxislabel = Utils.parseName(newValue, false)
settings.changed()
}
}
TextSetting {
id: yAxisLabel
height: 30
label: "Y Label"
width: settings.settingWidth
defValue: settings.yaxislabel
onChanged: function(newValue) {
settings.yaxislabel = Utils.parseName(newValue, false)
settings.changed()
}
}
Button {
id: saveDiagram
height: 30

View file

@ -30,49 +30,35 @@ Item {
property double min: 1
property string label
property string defValue
Text {
id: labelItem
height: 30
anchors.top: parent.top
verticalAlignment: TextInput.AlignVCenter
color: sysPalette.windowText
text: " "+ control.label +": "
}
Item {
anchors.centerIn: parent
width: labelItem.width + input.width
height: Math.max(labelItem.height, input.height)
Text {
id: labelItem
height: 30
anchors.top: parent.top
verticalAlignment: TextInput.AlignVCenter
color: sysPalette.windowText
text: " "+ control.label +": "
}
TextInput {
id: input
anchors.top: parent.top
anchors.left: labelItem.right
anchors.leftMargin: 5
width: control.width - labelItem.width
height: 30
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
color: sysPalette.windowText
focus: true
text: control.defValue
selectByMouse: true
onEditingFinished: {
var value = text
if(control.isInt) value = Math.max(control.min,parseInt(value).toString()=="NaN"?control.min:parseInt(value))
if(control.isDouble) value = Math.max(control.min,parseFloat(value).toString()=="NaN"?control.min:parseFloat(value))
if(value != "") control.changed(value)
}
}
Rectangle {
color: sysPalette.windowText
anchors.left: input.left
anchors.right: input.right
anchors.bottom: input.bottom
height: 2
TextField {
id: input
anchors.top: parent.top
anchors.left: labelItem.right
anchors.leftMargin: 5
width: control.width - labelItem.width - 5
height: 30
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
color: sysPalette.windowText
focus: true
text: control.defValue
selectByMouse: true
onEditingFinished: {
var value = text
if(control.isInt) value = Math.max(control.min,parseInt(value).toString()=="NaN"?control.min:parseInt(value))
if(control.isDouble) value = Math.max(control.min,parseFloat(value).toString()=="NaN"?control.min:parseFloat(value))
if(value != "") control.changed(value)
}
}
}

View file

@ -23,10 +23,21 @@
const parser = new ExprEval.Parser()
var evalVariables = { // Variables not provided by expr-eval.js, needs to be provided manualy
"pi": Math.PI,
"π": Math.PI,
"inf": Infinity,
"Infinity": Infinity,
"∞": Infinity,
"e": Math.E
}
class Expression {
constructor(expr) {
this.expr = expr
this.calc = parser.parse(expr).simplify()
this.cached = this.isConstant()
this.cachedValue = this.cached ? this.calc.evaluate(evalVariables) : null
}
isConstant() {
@ -34,19 +45,14 @@ class Expression {
}
execute(x = 1) {
return this.calc.evaluate({
"x": x,
"pi": Math.PI,
"π": Math.PI,
"inf": Infinity,
"Infinity": Infinity,
"∞": Infinity,
"e": Math.E
})
if(this.cached) return this.cachedValue
return this.calc.evaluate(Object.assign({'x': x}, evalVariables))
}
simplify(x = 1) {
return Utils.makeExpressionReadable(this.calc.substitute('x', x).simplify().toString())
var expr = this.calc.substitute('x', x).simplify()
if(expr.evaluate(evalVariables) == 0) return '0'
return Utils.makeExpressionReadable(expr.toString())
}
toEditableString() {

View file

@ -200,7 +200,7 @@ class Function extends ExecutableObject {
getReadableString() {
if(this.displayMode == 'application') {
return `${this.name}: ${this.inDomain}˃ ${this.outDomain}\n ${' '.repeat(this.name.length)}x ⸺˃ ${this.expression.toString()}`
return `${this.name}: ${this.inDomain}> ${this.outDomain}\n ${' '.repeat(this.name.length)}x ⸺> ${this.expression.toString()}`
} else {
return `${this.name}(x) = ${this.expression.toString()}`
}

View file

@ -119,27 +119,105 @@ function textsub(text) {
return ret
}
function simplifyExpression(str) {
var replacements = [
// Operations not done by parser.
[ // Removing parenthesis when content is only added from both sides.
/(^.?|[+-] |\()\(([^)(]+)\)(.?$| [+-]|\))/g,
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
],
[ // Removing parenthesis when content is only multiplied.
/(^.?|[*\/] |\()\(([^)(+-]+)\)(.?$| [*\/]|\))/g,
function(match, b4, middle, after) {return `${b4}${middle}${after}`}
],
[// Simplification additions/substractions.
/(^.?|[^*\/] |\()([-.\d]+) (\+|\-) (\([^)(]+\)|[^)(]+) (\+|\-) ([-.\d]+)(.?$| [^*\/]|\))/g,
function(match, b4, n1, op1, middle, op2, n2, after) {
var total
if(op2 == '+') {
total = parseFloat(n1) + parseFloat(n2)
} else {
total = parseFloat(n1) - parseFloat(n2)
}
return `${b4}${total} ${op1} ${middle}${after}`
}
],
[// Simplification multiplications/divisions.
/([-.\d]+) (\*|\/) (\([^)(]+\)|[^)(+-]+) (\*|\/) ([-.\d]+)/g,
function(match, n1, op1, middle, op2, n2) {
if(parseInt(n1) == n1 && parseInt(n2) == n2 && op2 == '/' &&
(parseInt(n1) / parseInt(n2)) % 1 != 0) {
// Non int result for int division.
return `(${n1} / ${n2}) ${op1} ${middle}`
} else {
if(op2 == '*') {
return `${parseFloat(n1) * parseFloat(n2)} ${op1} ${middle}`
} else {
return `${parseFloat(n1) / parseFloat(n2)} ${op1} ${middle}`
}
}
}
],
[// Starting & ending parenthesis if not needed.
/^\((.*)\)$/g,
function(match, middle) {
var str = middle
// Replace all groups
while(/\([^)(]+\)/g.test(str))
str = str.replace(/\([^)(]+\)/g, '')
// There shouldn't be any more parenthesis
// If there is, that means the 2 parenthesis are needed.
if(!str.includes(')') && !str.includes('(')) {
return middle
} else {
return `(${middle})`
}
}
],
// Simple simplifications
[/(\s|^|\()0 \* (\([^)(]+\))/g, '$10'],
[/(\s|^|\()0 \* ([^)(+-]+)/g, '$10'],
[/(\([^)(]\)) \* 0(\s|$|\))/g, '0$2'],
[/([^)(+-]) \* 0(\s|$|\))/g, '0$2'],
[/(\s|^|\()1 (\*|\/) /g, '$1'],
[/(\s|^|\()0 (\+|\-) /g, '$1'],
[/ (\*|\/) 1(\s|$|\))/g, '$2'],
[/ (\+|\-) 0(\s|$|\))/g, '$2'],
[/(^| |\() /g, '$1'],
[/ ($|\))/g, '$1'],
]
console.log(str)
// Replacements
replacements.forEach(function(replacement){
while(replacement[0].test(str))
str = str.replace(replacement[0], replacement[1])
})
return str
}
function makeExpressionReadable(str) {
var replacements = [
// variables
[/pi/g, 'π'],
[/Infinity/g, '∞'],
[/inf/g, '∞'],
// Other
[/ \* /g, '×'],
[/ \^ /g, '^'],
[/\^\(([^\^]+)\)/g, function(match, p1) { return textsup(p1) }],
[/\^([^ ]+)/g, function(match, p1) { return textsup(p1) }],
[/(\d|\))×/g, '$1'],
[/×(\d|\()/g, '$1'],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
// Doing it 4 times to be recursive until better implementation
[/\(([^)(+.\/-]+)\)/g, "$1"],
]
str = simplifyExpression(str)
// Replacements
replacements.forEach(function(replacement){
str = str.replace(replacement[0], replacement[1])
while(replacement[0].test(str))
str = str.replace(replacement[0], replacement[1])
})
return str
}
@ -184,7 +262,7 @@ function parseName(str, removeUnallowed = true) {
[/_\(([^\^]+)\)/g, function(match, p1) { return textsub(p1) }],
[/_([^ ]+)/g, function(match, p1) { return textsub(p1) }],
// Removing
[/[xπ\\∪∩\]\[ ()^/÷*×+=\d-]/g , function(match){console.log('removing', match); return ''}],
[/[xπ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''],
]
if(!removeUnallowed) replacements.pop()
// Replacements
@ -194,6 +272,7 @@ function parseName(str, removeUnallowed = true) {
return str
}
String.prototype.toLatinUppercase = function() {
return this.replace(/[a-z]/g, function(match){return match.toUpperCase()})
}

2
run.py
View file

@ -30,7 +30,7 @@ tempfile = tempfile.mkstemp(suffix = '.png')[1]
def get_linux_theme():
des = {
"KDE": "org.kde.desktop", # org.kde.desktop resolves to universal in PySide2.
"KDE": "fusion", # org.kde.desktop resolves to universal in PySide2.
"gnome": "default",
"lxqt": "fusion",
"mate": "fusion",