From cf586e853a9d8fcd654d0adcb87b3913c0de5042 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Mon, 29 Jun 2015 13:06:26 +0900 Subject: [PATCH] Work on modes --- server.go | 21 ++++------ static/scripts/grapher.js | 67 ++++++++++++-------------------- static/scripts/search.js | 18 +++------ types.go | 45 ++++++++++++++++------ util.go | 80 +++++++++++++++++---------------------- 5 files changed, 105 insertions(+), 126 deletions(-) diff --git a/server.go b/server.go index 92ed62d..f77ccce 100644 --- a/server.go +++ b/server.go @@ -52,38 +52,33 @@ func executeQuery(rw http.ResponseWriter, req *http.Request) { entries := getRecords(queryContext{geo, request.Profile, request.WalkingDist}) features := fixFeatures(request.Features) + modes := fixModes(request.Modes) - minScore := request.MinScore - if request.Bracket != nil { - bracket := namedBracket{ - request.Bracket.Name, - request.Bracket.Min, - request.Bracket.Max} - - minScore = calibrateMinScore(entries, features, bracket) - } - - foundEntries := findRecords(entries, features, minScore) + foundEntries := findRecords(entries, features, modes, request.MinScore) sorter := recordSorter{entries: foundEntries, key: request.SortKey, ascending: request.SortAsc} sorter.sort() response := jsonQueryResponse{ Count: len(foundEntries), Columns: make(map[string]jsonColumn), - MinScore: minScore, + MinScore: request.MinScore, Records: make([]jsonRecord, 0)} for name, value := range features { + mode, _ := modes[name] + column := jsonColumn{ Bracket: jsonBracket{Max: -1, Min: 1}, + Mode: mode.String(), Value: value, Steps: request.Resolution} hints := project( entries, features, + modes, name, - minScore, + request.MinScore, request.Resolution) for _, hint := range hints { diff --git a/static/scripts/grapher.js b/static/scripts/grapher.js index 87d04f3..28f00f8 100644 --- a/static/scripts/grapher.js +++ b/static/scripts/grapher.js @@ -88,7 +88,7 @@ var _height = 500; var _padding = 10; var _panelSize = 20; - var _buttonSize = 20; + var _modeSize = 20; var _tickSize = 5; var _width = 120; @@ -111,7 +111,7 @@ _densitySize, 0, _width - (_bracketSize + _densitySize), - _height - (_panelSize + _buttonSize) + _height - (_panelSize + _modeSize) ).attr({ cursor: 'crosshair', stroke: _borderColor, @@ -123,7 +123,7 @@ 0, 0, _densitySize, - _height - (_panelSize + _buttonSize) + _height - (_panelSize + _modeSize) ).attr({ stroke: _borderColor }); @@ -131,7 +131,7 @@ // panel _elements.panel = _canvas.rect( 0, - _height - (_panelSize + _buttonSize), + _height - (_panelSize + _modeSize), _width - _bracketSize, _panelSize ).attr({ @@ -141,7 +141,7 @@ // label _elements.label = _canvas.text( (_width - _bracketSize) / 2, - _height - (_panelSize / 2 + _buttonSize), + _height - (_panelSize / 2 + _modeSize), _name ).attr({ 'dominant-baseline': 'middle', @@ -153,10 +153,10 @@ _width - _bracketSize, 0, _bracketSize, - _height - (_panelSize + _buttonSize) + _height - (_panelSize + _modeSize) ).attr({ fill: _bracketColorBg - }).click(bracketClick); + }); // indiciator updateIndicator(_data.value); @@ -191,7 +191,7 @@ _elements.panel, _elements.tick, _elements.label, - _elements.button + _elements.mode ); _elements.group.transform( @@ -258,7 +258,7 @@ strokeWidth: 1, stroke: _bracketColorFg, 'stroke-dasharray': '5, 1' - }).click(bracketClick); + }); } if (_.has(_elements, 'bracketMin')) { @@ -297,19 +297,19 @@ } function updateMode() { - if (_.has(_elements, 'button')) { + if (_.has(_elements, 'mode')) { } else { - _elements.button = _canvas.text( + _elements.mode = _canvas.text( (_width - _bracketSize) / 2, - _height - _buttonSize / 2, - '' + _height - _modeSize / 2, + 'x' ).attr({ 'dominant-baseline': 'middle', 'text-anchor': 'middle' - }); + }).click(modeClick); } } @@ -344,25 +344,12 @@ return colorStops; } - function updateState(value, bracket) { - var updateBracket = bracket !== null; - var updateValue = value !== null; - - if (updateBracket) { - _data.bracket.max = _range.clamp(bracket.max); - _data.bracket.min = _range.clamp(bracket.min); - } - - if (updateValue) { - _data.value = _range.clamp(value); - } + function updateState(value, mode) { + _data.value = _range.clamp(value); + _data.mode = mode; if (_onStateChanged) { - _onStateChanged( - _name, - updateValue ? _data.value : null, - updateBracket ? _data.bracket : null - ); + _onStateChanged(_name, _data.value, _data.mode); } animateIndicator(_valueTrans, _data.value); @@ -462,19 +449,11 @@ function indicatorClick(event, x, y) { var rect = _canvas.node.getBoundingClientRect(); - - updateState(indicatorToValue(y - rect.top), null); + updateState(indicatorToValue(y - rect.top), _data.mode); } - function bracketClick(event, x, y) { - var mid = (_data.bracket.min + _data.bracket.max) / 2; - var rect = _canvas.node.getBoundingClientRect(); - - var dist = Math.abs(mid - bracketToValue(y - rect.top)); - dist = Math.min(dist, Math.abs(_range.max - mid)); - dist = Math.min(dist, Math.abs(_range.min - mid)); - - updateState(_data.value, {max: mid + dist, min: mid - dist}); + function modeClick(event, x, y) { + alert('mode clicked'); } this.update = function(data, scale) { @@ -492,6 +471,10 @@ _data.bracket = data.bracket; animateBracket(_bracketTrans, _data.bracket); } + if (_.has(data, 'mode')) { + _data.mode = data.mode; + updateMode(); + } }; createShapes(); diff --git a/static/scripts/search.js b/static/scripts/search.js index 9553131..acd3609 100644 --- a/static/scripts/search.js +++ b/static/scripts/search.js @@ -25,18 +25,9 @@ var _ctx = {}; - function onStateChanged(name, value, bracket) { + function onStateChanged(name, value, mode) { _ctx.query.features[name] = value; - if (bracket === null) { - _ctx.query.bracket = null; - } - else { - _ctx.query.bracket = { - name: name, - min: bracket.min, - max: bracket.max - }; - } + _ctx.query.modes[name] = mode; $.post('/query', JSON.stringify(_ctx.query), function(results) { saveSnapshot(results); @@ -87,7 +78,7 @@ function onSearch() { _ctx.query = { features: _ctx.query.features || {}, - bracket: null, + modes: _ctx.query.modes || {}, sortKey: _ctx.sortKey, sortAsc: _ctx.sortAsc, profile: getProfile(), @@ -132,7 +123,8 @@ columns[feature] = { value: column.value, hints: column.hints, - bracket: column.bracket + bracket: column.bracket, + mode: column.mode }; } diff --git a/types.go b/types.go index c9edcf3..af5b677 100644 --- a/types.go +++ b/types.go @@ -24,7 +24,16 @@ package main import "sort" +type modeType int + +const ( + ModeTypeNone modeType = iota + ModeTypeProduct + ModeTypeDist +) + type featureMap map[string]float64 +type modeMap map[string]modeType type jsonAccessRequest struct { Id int `json:"id"` @@ -37,11 +46,11 @@ type jsonGeoData struct { } type jsonQueryRequest struct { - Bracket *jsonNamedBracket `json:"bracket"` Features featureMap `json:"features"` Geo *jsonGeoData `json:"geo"` MaxResults int `json:"maxResults"` MinScore float64 `json:"minScore"` + Modes map[string]string `json:"modeMap"` Profile featureMap `json:"profile"` Resolution int `json:"resolution"` SortAsc bool `json:"sortAsc"` @@ -52,6 +61,7 @@ type jsonQueryRequest struct { type jsonColumn struct { Bracket jsonBracket `json:"bracket"` Hints []jsonProjection `json:"hints"` + Mode string `json:"mode"` Steps int `json:"steps"` Value float64 `json:"value"` } @@ -79,11 +89,6 @@ type jsonBracket struct { Max float64 `json:"max"` } -type jsonNamedBracket struct { - jsonBracket - Name string `json:"name"` -} - type jsonQueryResponse struct { Columns map[string]jsonColumn `json:"columns"` Count int `json:"count"` @@ -131,12 +136,6 @@ type geoData struct { longitude float64 } -type namedBracket struct { - name string - min float64 - max float64 -} - type record struct { accessCount int closestStn string @@ -196,3 +195,25 @@ func (s recordSorter) Less(i, j int) bool { func (s recordSorter) Swap(i, j int) { s.entries[i], s.entries[j] = s.entries[j], s.entries[i] } + +func (m modeType) String() string { + switch m { + case ModeTypeProduct: + return "prod" + case ModeTypeDist: + return "dist" + default: + return "invalid" + } +} + +func strToModeType(mode string) modeType { + switch mode { + case "prod": + return ModeTypeProduct + case "dist": + return ModeTypeDist + default: + return ModeTypeNone + } +} diff --git a/util.go b/util.go index 91fa479..a3146b5 100644 --- a/util.go +++ b/util.go @@ -47,6 +47,16 @@ func fixFeatures(features featureMap) featureMap { return fixedFeatures } +func fixModes(modes map[string]string) modeMap { + result := make(modeMap) + + for name, value := range modes { + result[name] = strToModeType(value) + } + + return result +} + func innerProduct(features1 featureMap, features2 featureMap) float64 { var result float64 for key, value1 := range features1 { @@ -57,19 +67,35 @@ func innerProduct(features1 featureMap, features2 featureMap) float64 { return result } -func walkMatches(entries records, features featureMap, minScore float64, callback func(record, float64)) { +func compare(features1 featureMap, features2 featureMap, modes modeMap) float64 { + var result float64 + for key, value1 := range features1 { + value2, _ := features2[key] + + switch mode, _ := modes[key]; mode { + case ModeTypeDist: + result += 1 - math.Abs(value1-value2)/2 + default: + result += value1 * value2 + } + } + + return result +} + +func walkMatches(entries records, features featureMap, modes modeMap, minScore float64, callback func(record, float64)) { for _, entry := range entries { - if score := innerProduct(features, entry.features); score >= minScore { + if score := compare(features, entry.features, modes); score >= minScore { callback(entry, score) } } } -func statRecords(entries records, features featureMap, minScore float64) (float64, int) { +func statRecords(entries records, features featureMap, modes modeMap, minScore float64) (float64, int) { var compatibility float64 var count int - walkMatches(entries, features, minScore, func(entry record, score float64) { + walkMatches(entries, features, modes, minScore, func(entry record, score float64) { compatibility += entry.compatibility count++ }) @@ -89,10 +115,10 @@ func stepRange(min, max float64, steps int, callback func(float64)) { } } -func findRecords(entries records, features featureMap, minScore float64) records { +func findRecords(entries records, features featureMap, modes modeMap, minScore float64) records { var foundEntries records - walkMatches(entries, features, minScore, func(entry record, score float64) { + walkMatches(entries, features, modes, minScore, func(entry record, score float64) { entry.score = score foundEntries = append(foundEntries, entry) }) @@ -100,45 +126,7 @@ func findRecords(entries records, features featureMap, minScore float64) records return foundEntries } -func calibrateMinScore(entries records, features featureMap, bracket namedBracket) float64 { - bestScoreRank := -math.MaxFloat64 - var bestMinScore float64 - - for minScore := float64(-len(features)); minScore <= float64(len(features)); minScore += 0.1 { - var scoreRank float64 - - for _, entry := range entries { - value, ok := entry.features[bracket.name] - if !ok { - continue - } - - score := innerProduct(features, entry.features) - if score < minScore { - continue - } - - if score > minScore { - if value >= bracket.min && value <= bracket.max { - dist := math.Abs(value - features[bracket.name]) - scoreRank += 1 / (dist * dist) - } else { - dist := math.Min(math.Abs(value-bracket.min), math.Abs(value-bracket.max)) - scoreRank -= 1 / (dist * dist) - } - } - } - - if scoreRank > bestScoreRank { - bestScoreRank = scoreRank - bestMinScore = minScore - } - } - - return bestMinScore -} - -func project(entries records, features featureMap, featureName string, minScore float64, steps int) []queryProjection { +func project(entries records, features featureMap, modes modeMap, featureName string, minScore float64, steps int) []queryProjection { sampleFeatures := make(featureMap) for key, value := range features { sampleFeatures[key] = value @@ -147,7 +135,7 @@ func project(entries records, features featureMap, featureName string, minScore var projection []queryProjection stepRange(-1.0, 1.0, steps, func(sample float64) { sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample - compatibility, count := statRecords(entries, sampleFeatures, minScore) + compatibility, count := statRecords(entries, sampleFeatures, modes, minScore) sample, sampleFeatures[featureName] = sampleFeatures[featureName], sample projection = append(projection, queryProjection{compatibility, count, sample})