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

This commit is contained in:
Adsooi 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 build
docs/html docs/html
.directory .directory
loggraph.kdev4 *.kdev4
*.json *.json
.kdev4 .kdev4
AccountFree.pro AccountFree.pro

View file

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

View file

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

View file

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

View file

@ -201,7 +201,7 @@ ListView {
isDouble: modelData[1] == 'number' isDouble: modelData[1] == 'number'
visible: ['Expression', 'Domain', 'string', 'number'].indexOf(modelData[1]) >= 0 visible: ['Expression', 'Domain', 'string', 'number'].indexOf(modelData[1]) >= 0
defValue: visible ? { 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()}, 'Domain': function(){return objEditor.obj[modelData[0]].toString()},
'string': function(){return objEditor.obj[modelData[0]]}, 'string': function(){return objEditor.obj[modelData[0]]},
'number': 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 QtQuick 2.12
import "js/utils.js" as Utils import "js/utils.js" as Utils
Grid { Column {
id: settings id: settings
height: 30*Math.max(1, Math.ceil(7 / columns)) height: 30*9 //30*Math.max(1, Math.ceil(7 / columns))
columns: Math.floor(width / settingWidth) //columns: Math.floor(width / settingWidth)
spacing: 10 spacing: 10
signal changed() signal changed()
property int settingWidth: 135 property int settingWidth: settings.width
property int xzoom: 100 property int xzoom: 100
property int yzoom: 10 property int yzoom: 10
property double xmin: 5/10 property double xmin: 5/10
property double ymax: 25 property double ymax: 25
property int yaxisstep: 4 property string yaxisstep: "4"
property string xaxislabel: "ω (rad/s)" property string xaxislabel: ""
property string yaxislabel: "G (dB)" property string yaxislabel: ""
property string saveFilename: "" property string saveFilename: ""
FileDialog { FileDialog {
@ -48,6 +48,10 @@ Grid {
root.saveDiagram(filePath) root.saveDiagram(filePath)
} else { } else {
root.loadDiagram(filePath) 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 { TextSetting {
id: yAxisStep id: yAxisStep
height: 30 height: 30
isInt: true //isInt: true
label: "Y Axis Step" label: "Y Axis Step"
width: settings.settingWidth width: settings.settingWidth
defValue: settings.yaxisstep 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 { Button {
id: copyToClipboard id: copyToClipboard
height: 30 height: 30
@ -127,30 +183,6 @@ Grid {
onClicked: root.copyDiagramToClipboard() 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 { Button {
id: saveDiagram id: saveDiagram
height: 30 height: 30

View file

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

View file

@ -23,10 +23,21 @@
const parser = new ExprEval.Parser() 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 { class Expression {
constructor(expr) { constructor(expr) {
this.expr = expr this.expr = expr
this.calc = parser.parse(expr).simplify() this.calc = parser.parse(expr).simplify()
this.cached = this.isConstant()
this.cachedValue = this.cached ? this.calc.evaluate(evalVariables) : null
} }
isConstant() { isConstant() {
@ -34,19 +45,14 @@ class Expression {
} }
execute(x = 1) { execute(x = 1) {
return this.calc.evaluate({ if(this.cached) return this.cachedValue
"x": x, return this.calc.evaluate(Object.assign({'x': x}, evalVariables))
"pi": Math.PI,
"π": Math.PI,
"inf": Infinity,
"Infinity": Infinity,
"∞": Infinity,
"e": Math.E
})
} }
simplify(x = 1) { 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() { toEditableString() {

View file

@ -200,7 +200,7 @@ class Function extends ExecutableObject {
getReadableString() { getReadableString() {
if(this.displayMode == 'application') { 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 { } else {
return `${this.name}(x) = ${this.expression.toString()}` return `${this.name}(x) = ${this.expression.toString()}`
} }

View file

@ -119,27 +119,105 @@ function textsub(text) {
return ret 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) { function makeExpressionReadable(str) {
var replacements = [ var replacements = [
// variables
[/pi/g, 'π'], [/pi/g, 'π'],
[/Infinity/g, '∞'], [/Infinity/g, '∞'],
[/inf/g, '∞'], [/inf/g, '∞'],
// Other
[/ \* /g, '×'], [/ \* /g, '×'],
[/ \^ /g, '^'], [/ \^ /g, '^'],
[/\^\(([^\^]+)\)/g, function(match, p1) { return textsup(p1) }], [/\^\(([^\^]+)\)/g, function(match, p1) { return textsup(p1) }],
[/\^([^ ]+)/g, function(match, p1) { return textsup(p1) }], [/\^([^ ]+)/g, function(match, p1) { return textsup(p1) }],
[/(\d|\))×/g, '$1'], [/(\d|\))×/g, '$1'],
[/×(\d|\()/g, '$1'], [/×(\d|\()/g, '$1'],
[/\(([^)(+.-]+)\)/g, "$1"], [/\(([^)(+.\/-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
[/\(([^)(+.-]+)\)/g, "$1"],
// Doing it 4 times to be recursive until better implementation
] ]
str = simplifyExpression(str)
// Replacements // Replacements
replacements.forEach(function(replacement){ 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 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) }],
[/_([^ ]+)/g, function(match, p1) { return textsub(p1) }], [/_([^ ]+)/g, function(match, p1) { return textsub(p1) }],
// Removing // Removing
[/[xπ\\∪∩\]\[ ()^/÷*×+=\d-]/g , function(match){console.log('removing', match); return ''}], [/[xπ\\∪∩\]\[ ()^/÷*×+=\d-]/g , ''],
] ]
if(!removeUnallowed) replacements.pop() if(!removeUnallowed) replacements.pop()
// Replacements // Replacements
@ -194,6 +272,7 @@ function parseName(str, removeUnallowed = true) {
return str return str
} }
String.prototype.toLatinUppercase = function() { String.prototype.toLatinUppercase = function() {
return this.replace(/[a-z]/g, function(match){return match.toUpperCase()}) 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(): def get_linux_theme():
des = { 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", "gnome": "default",
"lxqt": "fusion", "lxqt": "fusion",
"mate": "fusion", "mate": "fusion",