YAxisStep as expression, selection box for X & Y axis labels, simplifications for expressions.
This commit is contained in:
parent
487daa426a
commit
47a4ac67a9
11 changed files with 232 additions and 111 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -14,7 +14,7 @@ dist/
|
|||
build
|
||||
docs/html
|
||||
.directory
|
||||
loggraph.kdev4
|
||||
*.kdev4
|
||||
*.json
|
||||
.kdev4
|
||||
AccountFree.pro
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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]]}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,11 +31,6 @@ Item {
|
|||
property string label
|
||||
property string defValue
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: labelItem.width + input.width
|
||||
height: Math.max(labelItem.height, input.height)
|
||||
|
||||
Text {
|
||||
id: labelItem
|
||||
height: 30
|
||||
|
@ -46,12 +41,12 @@ Item {
|
|||
}
|
||||
|
||||
|
||||
TextInput {
|
||||
TextField {
|
||||
id: input
|
||||
anchors.top: parent.top
|
||||
anchors.left: labelItem.right
|
||||
anchors.leftMargin: 5
|
||||
width: control.width - labelItem.width
|
||||
width: control.width - labelItem.width - 5
|
||||
height: 30
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
horizontalAlignment: TextInput.AlignHCenter
|
||||
|
@ -66,13 +61,4 @@ Item {
|
|||
if(value != "") control.changed(value)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: sysPalette.windowText
|
||||
anchors.left: input.left
|
||||
anchors.right: input.right
|
||||
anchors.bottom: input.bottom
|
||||
height: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()}`
|
||||
}
|
||||
|
|
|
@ -119,26 +119,104 @@ 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){
|
||||
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
2
run.py
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue